Rearrange trunk to support gentoolkit version 0.3. Split into gentoolkit, gentoolkit...
[gentoolkit.git] / trunk / src / eclean / eclean
diff --git a/trunk/src/eclean/eclean b/trunk/src/eclean/eclean
deleted file mode 100644 (file)
index 55cc2a7..0000000
+++ /dev/null
@@ -1,838 +0,0 @@
-#!/usr/bin/python
-# Copyright 2003-2005 Gentoo Foundation
-# Distributed under the terms of the GNU General Public License v2
-# $Header: $
-
-from __future__ import with_statement
-
-###############################################################################
-# Meta:
-__author__ = "Thomas de Grenier de Latour (tgl)"
-__email__ = "degrenier@easyconnect.fr"
-__version__ = "0.4.1"
-__productname__ = "eclean"
-__description__ = "A cleaning tool for Gentoo distfiles and binaries."
-
-
-###############################################################################
-# Python imports:
-
-import sys
-import os, stat
-import re
-import time
-import getopt
-import fpformat
-import signal
-try:
-       import portage
-except ImportError:
-       sys.path.insert(0, "/usr/lib/portage/pym")
-       import portage
-try:
-       from portage.output import *
-except ImportError:
-       from output import *
-
-listdir = portage.listdir
-
-###############################################################################
-# Misc. shortcuts to some portage stuff:
-port_settings = portage.settings
-distdir = port_settings["DISTDIR"]
-pkgdir = port_settings["PKGDIR"]
-
-###############################################################################
-# printVersion:
-def printVersion():
-       print "%s (version %s) - %s" \
-                       % (__productname__, __version__, __description__)
-       print "Author: %s <%s>" % (__author__,__email__)
-       print "Copyright 2003-2005 Gentoo Foundation"
-       print "Distributed under the terms of the GNU General Public License v2"
-
-
-###############################################################################
-# printUsage: print help message. May also print partial help to stderr if an
-# error from {'options','actions'} is specified.
-def printUsage(error=None,help=None):
-       out = sys.stdout
-       if error: out = sys.stderr
-       if not error in ('actions', 'global-options', \
-                       'packages-options', 'distfiles-options', \
-                       'merged-packages-options', 'merged-distfiles-options', \
-                       'time', 'size'):
-               error = None
-       if not error and not help: help = 'all'
-       if error == 'time':
-               eerror("Wrong time specification")
-               print >>out, "Time specification should be an integer followed by a"+ \
-                               " single letter unit."
-               print >>out, "Available units are: y (years), m (months), w (weeks), "+ \
-                               "d (days) and h (hours)."
-               print >>out, "For instance: \"1y\" is \"one year\", \"2w\" is \"two"+ \
-                               " weeks\", etc. "
-               return
-       if error == 'size':
-               eerror("Wrong size specification")
-               print >>out, "Size specification should be an integer followed by a"+ \
-                               " single letter unit."
-               print >>out, "Available units are: G, M, K and B."
-               print >>out, "For instance: \"10M\" is \"ten megabytes\", \"200K\" "+ \
-                               "is \"two hundreds kilobytes\", etc."
-               return
-       if error in ('global-options', 'packages-options', 'distfiles-options', \
-                       'merged-packages-options', 'merged-distfiles-options',):
-               eerror("Wrong option on command line.")
-               print >>out
-       elif error == 'actions':
-               eerror("Wrong or missing action name on command line.")
-               print >>out
-       print >>out, white("Usage:")
-       if error in ('actions','global-options', 'packages-options', \
-       'distfiles-options') or help == 'all':
-               print >>out, " "+turquoise(__productname__), \
-                       yellow("[global-option] ..."), \
-                       green("<action>"), \
-                       yellow("[action-option] ...")
-       if error == 'merged-distfiles-options' or help in ('all','distfiles'):
-               print >>out, " "+turquoise(__productname__+'-dist'), \
-                       yellow("[global-option, distfiles-option] ...")
-       if error == 'merged-packages-options' or help in ('all','packages'):
-               print >>out, " "+turquoise(__productname__+'-pkg'), \
-                       yellow("[global-option, packages-option] ...")
-       if error in ('global-options', 'actions'):
-               print >>out, " "+turquoise(__productname__), \
-                       yellow("[--help, --version]")
-       if help == 'all':
-               print >>out, " "+turquoise(__productname__+"(-dist,-pkg)"), \
-                       yellow("[--help, --version]")
-       if error == 'merged-packages-options' or help == 'packages':
-               print >>out, " "+turquoise(__productname__+'-pkg'), \
-                       yellow("[--help, --version]")
-       if error == 'merged-distfiles-options' or help == 'distfiles':
-               print >>out, " "+turquoise(__productname__+'-dist'), \
-                       yellow("[--help, --version]")
-       print >>out
-       if error in ('global-options', 'merged-packages-options', \
-       'merged-distfiles-options') or help:
-               print >>out, "Available global", yellow("options")+":"
-               print >>out, yellow(" -C, --nocolor")+ \
-                       "             - turn off colors on output"
-               print >>out, yellow(" -d, --destructive")+ \
-                       "         - only keep the minimum for a reinstallation"
-               print >>out, yellow(" -e, --exclude-file=<path>")+ \
-                       " - path to the exclusion file"
-               print >>out, yellow(" -i, --interactive")+ \
-                       "         - ask confirmation before deletions"
-               print >>out, yellow(" -n, --package-names")+ \
-                       "       - protect all versions (when --destructive)"
-               print >>out, yellow(" -p, --pretend")+ \
-                       "             - only display what would be cleaned"
-               print >>out, yellow(" -q, --quiet")+ \
-                       "               - be as quiet as possible"
-               print >>out, yellow(" -t, --time-limit=<time>")+ \
-                       "   - don't delete files modified since "+yellow("<time>")
-               print >>out, "   "+yellow("<time>"), "is a duration: \"1y\" is"+ \
-                               " \"one year\", \"2w\" is \"two weeks\", etc. "
-               print >>out, "   "+"Units are: y (years), m (months), w (weeks), "+ \
-                               "d (days) and h (hours)."
-               print >>out, yellow(" -h, --help")+ \
-                       "                - display the help screen"
-               print >>out, yellow(" -V, --version")+ \
-                       "             - display version info"
-               print >>out
-       if error == 'actions' or help == 'all':
-               print >>out, "Available", green("actions")+":"
-               print >>out, green(" packages")+ \
-                       "      - clean outdated binary packages from:"
-               print >>out, "                  ",teal(pkgdir)
-               print >>out, green(" distfiles")+ \
-                       "     - clean outdated packages sources files from:"
-               print >>out, "                  ",teal(distdir)
-               print >>out
-       if error in ('packages-options','merged-packages-options') \
-       or help in ('all','packages'):
-               print >>out, "Available", yellow("options"),"for the", \
-                               green("packages"),"action:"
-               print >>out, yellow(" NONE  :)")
-               print >>out
-       if error in ('distfiles-options', 'merged-distfiles-options') \
-       or help in ('all','distfiles'):
-               print >>out, "Available", yellow("options"),"for the", \
-                               green("distfiles"),"action:"
-               print >>out, yellow(" -f, --fetch-restricted")+ \
-                       "   - protect fetch-restricted files (when --destructive)"
-               print >>out, yellow(" -s, --size-limit=<size>")+ \
-                       "  - don't delete distfiles bigger than "+yellow("<size>")
-               print >>out, "   "+yellow("<size>"), "is a size specification: "+ \
-                               "\"10M\" is \"ten megabytes\", \"200K\" is"
-               print >>out, "   "+"\"two hundreds kilobytes\", etc.  Units are: "+ \
-                               "G, M, K and B."
-               print >>out
-       print >>out, "More detailed instruction can be found in", \
-                       turquoise("`man %s`" % __productname__)
-
-
-###############################################################################
-# einfo: display an info message depending on a color mode
-def einfo(message="", nocolor=False):
-       if not nocolor: prefix = " "+green('*')
-       else: prefix = ">>>"
-       print prefix,message
-
-
-###############################################################################
-# eerror: display an error depending on a color mode
-def eerror(message="", nocolor=False):
-       if not nocolor: prefix = " "+red('*')
-       else: prefix = "!!!"
-       print >>sys.stderr,prefix,message
-
-
-###############################################################################
-# eprompt: display a user question depending on a color mode.
-def eprompt(message, nocolor=False):
-       if not nocolor: prefix = " "+red('>')+" "
-       else: prefix = "??? "
-       sys.stdout.write(prefix+message)
-       sys.stdout.flush()
-
-
-###############################################################################
-# prettySize: integer -> byte/kilo/mega/giga converter. Optionnally justify the
-# result. Output is a string.
-def prettySize(size,justify=False):
-       units = [" G"," M"," K"," B"]
-       approx = 0
-       while len(units) and size >= 1000:
-               approx = 1
-               size = size / 1024.
-               units.pop()
-       sizestr = fpformat.fix(size,approx)+units[-1]
-       if justify: 
-               sizestr = " " + blue("[ ") + " "*(7-len(sizestr)) \
-                         + green(sizestr) + blue(" ]")
-       return sizestr
-
-
-###############################################################################
-# yesNoAllPrompt: print a prompt until user answer in yes/no/all. Return a 
-# boolean for answer, and also may affect the 'accept_all' option.
-# Note: i gave up with getch-like functions, to much bugs in case of escape
-# sequences. Back to raw_input.
-def yesNoAllPrompt(myoptions,message="Do you want to proceed?"):
-       user_string="xxx"
-       while not user_string.lower() in ["","y","n","a","yes","no","all"]:
-               eprompt(message+" [Y/n/a]: ", myoptions['nocolor'])
-               user_string = raw_input()
-       if user_string.lower() in ["a","all"]:
-               myoptions['accept_all'] = True
-       myanswer = user_string.lower() in ["","y","a","yes","all"]
-       return myanswer
-
-
-###############################################################################
-# ParseArgsException: for parseArgs() -> main() communication
-class ParseArgsException(Exception):
-       def __init__(self, value):
-               self.value = value
-       def __str__(self):
-               return repr(self.value)
-
-
-###############################################################################
-# parseSize: convert a file size "Xu" ("X" is an integer, and "u" in [G,M,K,B])
-# into an integer (file size in Bytes). Raises ParseArgsException('size') in
-# case of failure.
-def parseSize(size):
-       myunits = { \
-               'G': (1024**3), \
-               'M': (1024**2), \
-               'K': 1024, \
-               'B': 1 \
-       }
-       try:
-               mymatch = re.match(r"^(?P<value>\d+)(?P<unit>[GMKBgmkb])?$",size)
-               mysize = int(mymatch.group('value'))
-               if mymatch.group('unit'):
-                       mysize *= myunits[mymatch.group('unit').capitalize()]
-       except:
-               raise ParseArgsException('size')
-       return mysize
-
-
-###############################################################################
-# parseTime: convert a duration "Xu" ("X" is an int, and "u" a time unit in 
-# [Y,M,W,D,H]) into an integer which is a past EPOCH date. 
-# Raises ParseArgsException('time') in case of failure.
-# (yep, big approximations inside... who cares?)
-def parseTime(timespec):
-       myunits = {'H' : (60 * 60)}
-       myunits['D'] = myunits['H'] * 24
-       myunits['W'] = myunits['D'] * 7
-       myunits['M'] = myunits['D'] * 30
-       myunits['Y'] = myunits['D'] * 365
-       try:
-               # parse the time specification
-               mymatch = re.match(r"^(?P<value>\d+)(?P<unit>[YMWDHymwdh])?$",timespec)
-               myvalue = int(mymatch.group('value'))
-               if not mymatch.group('unit'): myunit = 'D'
-               else: myunit = mymatch.group('unit').capitalize()
-       except: raise ParseArgsException('time')
-       # calculate the limit EPOCH date
-       mytime = time.time() - (myvalue * myunits[myunit])
-       return mytime
-
-
-###############################################################################
-# parseCmdLine: parse the command line arguments. Raise exceptions on errors or
-# non-action modes (help/version). Returns an action, and affect the options
-# dict.
-def parseArgs(myoptions={}):
-
-       # local function for interpreting command line options 
-       # and setting myoptions accordingly
-       def optionSwitch(myoption,opts,action=None):
-               return_code = True
-               for o, a in opts:
-                       if o in ("-h", "--help"):
-                               if action: raise ParseArgsException('help-'+action)
-                               else: raise ParseArgsException('help')
-                       elif o in ("-V", "--version"):
-                               raise ParseArgsException('version')
-                       elif o in ("-C", "--nocolor"):
-                               myoptions['nocolor'] = True
-                               nocolor()
-                       elif o in ("-d", "--destructive"):
-                               myoptions['destructive'] = True
-                       elif o in ("-i", "--interactive") and not myoptions['pretend']:
-                               myoptions['interactive'] = True
-                       elif o in ("-p", "--pretend"):
-                               myoptions['pretend'] = True
-                               myoptions['interactive'] = False
-                       elif o in ("-q", "--quiet"):
-                               myoptions['quiet'] = True
-                       elif o in ("-t", "--time-limit"):
-                               myoptions['time-limit'] = parseTime(a)
-                       elif o in ("-e", "--exclude-file"):
-                               myoptions['exclude-file'] = a
-                       elif o in ("-n", "--package-names"):
-                               myoptions['package-names'] = True
-                       elif o in ("-f", "--fetch-restricted"):
-                               myoptions['fetch-restricted'] = True
-                       elif o in ("-s", "--size-limit"):
-                               myoptions['size-limit'] = parseSize(a)
-                       else: return_code = False
-               # sanity check of --destructive only options:
-               for myopt in ('fetch-restricted', 'package-names'):
-                       if (not myoptions['destructive']) and myoptions[myopt]:
-                               if not myoptions['quiet']:      
-                                       eerror("--%s only makes sense in --destructive mode." \
-                                                       % myopt, myoptions['nocolor'])
-                               myoptions[myopt] = False
-               return return_code
-
-       # here are the different allowed command line options (getopt args)
-       getopt_options = {'short':{}, 'long':{}}
-       getopt_options['short']['global'] = "Cdipqe:t:nhV"
-       getopt_options['long']['global'] = ["nocolor", "destructive", \
-                       "interactive", "pretend", "quiet", "exclude-file=", "time-limit=", \
-                       "package-names", "help", "version"]
-       getopt_options['short']['distfiles'] = "fs:"
-       getopt_options['long']['distfiles'] = ["fetch-restricted", "size-limit="]
-       getopt_options['short']['packages'] = ""
-       getopt_options['long']['packages'] = [""]
-       # set default options, except 'nocolor', which is set in main()
-       myoptions['interactive'] = False
-       myoptions['pretend'] = False
-       myoptions['quiet'] = False
-       myoptions['accept_all'] = False
-       myoptions['destructive'] = False
-       myoptions['time-limit'] = 0
-       myoptions['package-names'] = False
-       myoptions['fetch-restricted'] = False
-       myoptions['size-limit'] = 0
-       # if called by a well-named symlink, set the acction accordingly:
-       myaction = None
-       if os.path.basename(sys.argv[0]) in \
-                       (__productname__+'-pkg', __productname__+'-packages'):
-               myaction = 'packages'
-       elif os.path.basename(sys.argv[0]) in \
-                       (__productname__+'-dist', __productname__+'-distfiles'):
-               myaction = 'distfiles'
-       # prepare for the first getopt
-       if myaction:
-               short_opts = getopt_options['short']['global'] \
-                               + getopt_options['short'][myaction]
-               long_opts = getopt_options['long']['global'] \
-                               + getopt_options['long'][myaction]
-               opts_mode = 'merged-'+myaction
-       else:
-               short_opts = getopt_options['short']['global']
-               long_opts = getopt_options['long']['global']
-               opts_mode = 'global'
-       # apply getopts to command line, show partial help on failure
-       try: opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
-       except: raise ParseArgsException(opts_mode+'-options')
-       # set myoptions accordingly
-       optionSwitch(myoptions,opts,action=myaction)
-       # if action was already set, there should be no more args
-       if myaction and len(args): raise ParseArgsException(opts_mode+'-options')
-       # if action was set, there is nothing left to do
-       if myaction: return myaction
-       # So, we are in "eclean --foo action --bar" mode. Parse remaining args...
-       # Only two actions are allowed: 'packages' and 'distfiles'.
-       if not len(args) or not args[0] in ('packages','distfiles'):
-               raise ParseArgsException('actions')
-       myaction = args.pop(0)
-       # parse the action specific options
-       try: opts, args = getopt.getopt(args, \
-                       getopt_options['short'][myaction], \
-                       getopt_options['long'][myaction])
-       except: raise ParseArgsException(myaction+'-options')
-       # set myoptions again, for action-specific options
-       optionSwitch(myoptions,opts,action=myaction)
-       # any remaning args? Then die!
-       if len(args): raise ParseArgsException(myaction+'-options')
-       # returns the action. Options dictionary is modified by side-effect.
-       return myaction
-
-###############################################################################
-# isValidCP: check wether a string is a valid cat/pkg-name
-# This is for 2.0.51 vs. CVS HEAD compatibility, i've not found any function
-# for that which would exists in both. Weird...
-def isValidCP(cp):
-       if not '/' in cp: return False
-       try: portage.cpv_getkey(cp+"-0")
-       except: return False
-       else: return True
-
-
-###############################################################################
-# ParseExcludeFileException: for parseExcludeFile() -> main() communication
-class ParseExcludeFileException(Exception):
-       def __init__(self, value):
-               self.value = value
-       def __str__(self):
-               return repr(self.value)
-
-
-###############################################################################
-# parseExcludeFile: parses an exclusion file, returns an exclusion dictionnary
-# Raises ParseExcludeFileException in case of fatal error.
-def parseExcludeFile(filepath):
-       excl_dict = { \
-                       'categories':{}, \
-                       'packages':{}, \
-                       'anti-packages':{}, \
-                       'garbage':{} }
-       try: file = open(filepath,"r")
-       except IOError:
-               raise ParseExcludeFileException("Could not open exclusion file.")
-       filecontents = file.readlines()
-       file.close()
-       cat_re = re.compile('^(?P<cat>[a-zA-Z0-9]+-[a-zA-Z0-9]+)(/\*)?$')
-       cp_re = re.compile('^(?P<cp>[-a-zA-Z0-9_]+/[-a-zA-Z0-9_]+)$')   
-       for line in filecontents:
-               line = line.strip()
-               if not len(line): continue
-               if line[0] == '#': continue
-               try: mycat = cat_re.match(line).group('cat')
-               except: pass
-               else: 
-                       if not mycat in portage.settings.categories:
-                               raise ParseExcludeFileException("Invalid category: "+mycat)
-                       excl_dict['categories'][mycat] = None
-                       continue
-               dict_key = 'packages'
-               if line[0] == '!':
-                       dict_key = 'anti-packages'
-                       line = line[1:]
-               try:
-                       mycp = cp_re.match(line).group('cp')
-                       if isValidCP(mycp):
-                               excl_dict[dict_key][mycp] = None
-                               continue
-                       else: raise ParseExcludeFileException("Invalid cat/pkg: "+mycp)
-               except: pass
-               #raise ParseExcludeFileException("Invalid line: "+line)
-               try:
-                       excl_dict['garbage'][line] = re.compile(line)
-               except:
-                       try:
-                               excl_dict['garbage'][line] = re.compile(re.escape(line))
-                       except:
-                               raise ParseExcludeFileException("Invalid file name/regular expression: "+line)
-       return excl_dict
-
-
-###############################################################################
-# exclDictExpand: returns a dictionary of all CP from porttree which match
-# the exclusion dictionary
-def exclDictExpand(excl_dict):
-       mydict = {}
-       if 'categories' in excl_dict:
-               # XXX: i smell an access to something which is really out of API...
-               for mytree in portage.portdb.porttrees:
-                       for mycat in excl_dict['categories']:
-                               for mypkg in listdir(os.path.join(mytree,mycat),ignorecvs=1):
-                                       mydict[mycat+'/'+mypkg] = None
-       if 'packages' in excl_dict:
-               for mycp in excl_dict['packages']:
-                       mydict[mycp] = None
-       if 'anti-packages' in excl_dict:
-               for mycp in excl_dict['anti-packages']:
-                       if mycp in mydict:
-                               del mydict[mycp]
-       return mydict
-
-
-###############################################################################
-# exclDictMatch: checks whether a CP matches the exclusion rules
-def exclDictMatch(excl_dict,pkg):
-       if 'anti-packages' in excl_dict \
-          and pkg in excl_dict['anti-packages']:
-               return False
-       if 'packages' in excl_dict \
-          and pkg in excl_dict['packages']:
-               return True
-       mycat = pkg.split('/')[0]
-       if 'categories' in excl_dict \
-          and mycat in excl_dict['categories']:
-               return True
-       return False
-
-
-###############################################################################
-# findDistfiles: find all obsolete distfiles.
-# XXX: what about cvs ebuilds? i should install some to see where it goes...
-def findDistfiles( \
-               myoptions, \
-               exclude_dict={}, \
-               destructive=False,\
-               fetch_restricted=False, \
-               package_names=False, \
-               time_limit=0, \
-               size_limit=0,):
-       # this regexp extracts files names from SRC_URI. It is not very precise,
-       # but we don't care (may return empty strings, etc.), since it is fast.
-       file_regexp = re.compile('([a-zA-Z0-9_,\.\-\+\~]*)[\s\)]')
-       clean_dict = {}
-       keep = []
-       pkg_dict = {}
-
-       # create a big CPV->SRC_URI dict of packages whose distfiles should be kept
-       if (not destructive) or fetch_restricted:
-               # list all CPV from portree (yeah, that takes time...)
-               for package in portage.portdb.cp_all():
-                       for my_cpv in portage.portdb.cp_list(package):
-                               # get SRC_URI and RESTRICT from aux_get
-                               try: (src_uri,restrict) = \
-                                       portage.portdb.aux_get(my_cpv,["SRC_URI","RESTRICT"])
-                               except KeyError: continue
-                               # keep either all or fetch-restricted only
-                               if (not destructive) or ('fetch' in restrict):
-                                       pkg_dict[my_cpv] = src_uri
-       if destructive:
-               if not package_names:
-                       # list all CPV from vartree
-                       pkg_list = portage.db[portage.root]["vartree"].dbapi.cpv_all()
-               else:
-                       # list all CPV from portree for CP in vartree
-                       pkg_list = []
-                       for package in portage.db[portage.root]["vartree"].dbapi.cp_all():
-                               pkg_list += portage.portdb.cp_list(package)
-               for my_cp in exclDictExpand(exclude_dict):
-                       # add packages from the exclude file
-                       pkg_list += portage.portdb.cp_list(my_cp)
-               for my_cpv in pkg_list:
-                       # skip non-existing CPV (avoids ugly aux_get messages)
-                       if not portage.portdb.cpv_exists(my_cpv): continue
-                       # get SRC_URI from aux_get
-                       try: pkg_dict[my_cpv] = \
-                                       portage.portdb.aux_get(my_cpv,["SRC_URI"])[0]
-                       except KeyError: continue
-               del pkg_list
-
-       # create a dictionary of files which should be deleted
-       if not (os.path.isdir(distdir)):
-               eerror("%s does not appear to be a directory." % distdir, myoptions['nocolor'])
-               eerror("Please set DISTDIR to a sane value.", myoptions['nocolor'])
-               eerror("(Check your /etc/make.conf and environment).", myoptions['nocolor'])
-               exit(1)
-       for file in os.listdir(distdir):
-               filepath = os.path.join(distdir, file)
-               try: file_stat = os.stat(filepath)
-               except: continue
-               if not stat.S_ISREG(file_stat[stat.ST_MODE]): continue
-               if size_limit and (file_stat[stat.ST_SIZE] >= size_limit):
-                       continue
-               if time_limit and (file_stat[stat.ST_MTIME] >= time_limit):
-                       continue
-               if 'garbage' in exclude_dict:
-                       # Try to match file name directly
-                       if file in exclude_dict['garbage']:
-                               file_match = True
-                       # See if file matches via regular expression matching
-                       else:
-                               file_match = False
-                               for file_entry in exclude_dict['garbage']:
-                                       if exclude_dict['garbage'][file_entry].match(file):
-                                               file_match = True
-                                               break
-
-                       if file_match:
-                               continue
-               # this is a candidate for cleaning
-               clean_dict[file]=[filepath]
-       # remove files owned by some protected packages
-       for my_cpv in pkg_dict:
-               for file in file_regexp.findall(pkg_dict[my_cpv]+"\n"):
-                       if file in clean_dict:
-                               del clean_dict[file]
-               # no need to waste IO time if there is nothing left to clean
-               if not len(clean_dict): return clean_dict
-       return clean_dict
-
-
-###############################################################################
-# findPackages: find all obsolete binary packages.
-# XXX: packages are found only by symlinks. Maybe i should also return .tbz2
-#      files from All/ that have no corresponding symlinks.
-def findPackages( \
-               myoptions, \
-               exclude_dict={}, \
-               destructive=False, \
-               time_limit=0, \
-               package_names=False):
-       clean_dict = {}
-       # create a full package dictionary
-       
-       if not (os.path.isdir(pkgdir)):
-               eerror("%s does not appear to be a directory." % pkgdir, myoptions['nocolor'])
-               eerror("Please set PKGDIR to a sane value.", myoptions['nocolor'])
-               eerror("(Check your /etc/make.conf and environment).", myoptions['nocolor'])
-               exit(1)
-       for root, dirs, files in os.walk(pkgdir):
-               if root[-3:] == 'All': continue
-               for file in files:
-                       if not file[-5:] == ".tbz2":
-                               # ignore non-tbz2 files
-                               continue
-                       path = os.path.join(root, file)
-                       category = os.path.split(root)[-1]
-                       cpv = category+"/"+file[:-5]
-                       mystat = os.lstat(path)
-                       if time_limit and (mystat[stat.ST_MTIME] >= time_limit):
-                               # time-limit exclusion
-                               continue
-                       # dict is cpv->[files] (2 files in general, because of symlink)
-                       clean_dict[cpv] = [path]
-                       #if os.path.islink(path):
-                       if stat.S_ISLNK(mystat[stat.ST_MODE]):
-                               clean_dict[cpv].append(os.path.realpath(path))
-       # keep only obsolete ones
-       if destructive:
-               mydbapi = portage.db[portage.root]["vartree"].dbapi
-               if package_names: cp_all = dict.fromkeys(mydbapi.cp_all())
-               else: cp_all = {}
-       else:
-               mydbapi = portage.db[portage.root]["porttree"].dbapi
-               cp_all = {}
-       for mycpv in clean_dict.keys():
-               if exclDictMatch(exclude_dict,portage.cpv_getkey(mycpv)):
-                       # exclusion because of the exclude file
-                       del clean_dict[mycpv]
-                       continue
-               if mydbapi.cpv_exists(mycpv):
-                       # exclusion because pkg still exists (in porttree or vartree)
-                       del clean_dict[mycpv]
-                       continue
-               if portage.cpv_getkey(mycpv) in cp_all:
-                       # exlusion because of --package-names
-                       del clean_dict[mycpv]
-
-       return clean_dict
-
-
-###############################################################################
-# doCleanup: takes a dictionnary {'display name':[list of files]}. Calculate
-# size of each entry for display, prompt user if needed, delete files if needed
-# and return the total size of files that [have been / would be] deleted.
-def doCleanup(clean_dict,action,myoptions):
-       # define vocabulary of this action
-       if action == 'distfiles': file_type = 'file'
-       else: file_type = 'binary package'
-       # sorting helps reading
-       clean_keys = clean_dict.keys()
-       clean_keys.sort()
-       clean_size = 0
-       # clean all entries one by one
-       for mykey in clean_keys:
-               key_size = 0
-               for file in clean_dict[mykey]:
-                       # get total size for an entry (may be several files, and
-                       # symlinks count zero)
-                       if os.path.islink(file): continue
-                       try: key_size += os.path.getsize(file)
-                       except: eerror("Could not read size of "+file, \
-                                      myoptions['nocolor'])
-               if not myoptions['quiet']:
-                       # pretty print mode
-                       print prettySize(key_size,True),teal(mykey)
-               elif myoptions['pretend'] or myoptions['interactive']:
-                       # file list mode
-                       for file in clean_dict[mykey]: print file
-               #else: actually delete stuff, but don't print anything
-               if myoptions['pretend']: clean_size += key_size
-               elif not myoptions['interactive'] \
-                    or myoptions['accept_all'] \
-                    or yesNoAllPrompt(myoptions, \
-                                      "Do you want to delete this " \
-                                      + file_type+"?"):
-                       # non-interactive mode or positive answer. 
-                       # For each file, try to delete the file and clean it out
-                       # of Packages metadata file
-                       if action == 'packages':
-                               metadata = portage.getbinpkg.PackageIndex()
-                               with open(os.path.join(pkgdir, 'Packages')) as metadata_file:
-                                       metadata.read(metadata_file)
-                       for file in clean_dict[mykey]:
-                               # ...get its size...
-                               filesize = 0
-                               if not os.path.exists(file): continue
-                               if not os.path.islink(file):
-                                       try: filesize = os.path.getsize(file)
-                                       except: eerror("Could not read size of "\
-                                                      +file, myoptions['nocolor'])
-                               # ...and try to delete it.
-                               try:
-                                       os.unlink(file)
-                               except:
-                                       eerror("Could not delete "+file, \
-                                        myoptions['nocolor'])
-                               # only count size if successfully deleted
-                               else:
-                                       clean_size += filesize
-                                       if action == 'packages':
-                                               metadata.packages[:] = [p for p in metadata.packages if 'CPV' in p and p['CPV'] != file]
-                               
-                       if action == 'packages':
-                               with open(os.path.join(pkgdir, 'Packages'), 'w') as metadata_file:
-                                       metadata.write(metadata_file)
-
-       # return total size of deleted or to delete files
-       return clean_size
-
-
-###############################################################################
-# doAction: execute one action, ie display a few message, call the right find*
-# function, and then call doCleanup with its result.
-def doAction(action,myoptions,exclude_dict={}):
-       # define vocabulary for the output
-       if action == 'packages': files_type = "binary packages"
-       else: files_type = "distfiles"
-       # find files to delete, depending on the action
-       if not myoptions['quiet']:
-               einfo("Building file list for "+action+" cleaning...", \
-                     myoptions['nocolor'])
-       if action == 'packages':
-               clean_dict = findPackages(
-                       myoptions, \
-                       exclude_dict=exclude_dict, \
-                       destructive=myoptions['destructive'], \
-                       package_names=myoptions['package-names'], \
-                       time_limit=myoptions['time-limit'])
-       else: 
-               clean_dict = findDistfiles( \
-                       myoptions, \
-                       exclude_dict=exclude_dict, \
-                       destructive=myoptions['destructive'], \
-                       fetch_restricted=myoptions['fetch-restricted'], \
-                       package_names=myoptions['package-names'], \
-                       time_limit=myoptions['time-limit'], \
-                       size_limit=myoptions['size-limit'])
-       # actually clean files if something was found
-       if len(clean_dict.keys()):
-               # verbose pretend message
-               if myoptions['pretend'] and not myoptions['quiet']:
-                       einfo("Here are "+files_type+" that would be deleted:", \
-                             myoptions['nocolor'])
-               # verbose non-pretend message
-               elif not myoptions['quiet']:
-                       einfo("Cleaning "+files_type+"...",myoptions['nocolor'])
-               # do the cleanup, and get size of deleted files
-               clean_size = doCleanup(clean_dict,action,myoptions)
-               # vocabulary for final message
-               if myoptions['pretend']: verb = "would be"
-               else: verb = "has been"
-               # display freed space
-               if not myoptions['quiet']:
-                       einfo("Total space that "+verb+" freed in " \
-                             + action + " directory: " \
-                             + red(prettySize(clean_size)), \
-                             myoptions['nocolor'])
-       # nothing was found, return
-       elif not myoptions['quiet']:
-               einfo("Your "+action+" directory was already clean.", \
-                     myoptions['nocolor'])
-
-
-###############################################################################
-# main: parse command line and execute all actions
-def main():
-       # set default options
-       myoptions = {}
-       myoptions['nocolor'] = port_settings["NOCOLOR"] in ('yes','true') \
-                              and sys.stdout.isatty()
-       if myoptions['nocolor']: nocolor()
-       # parse command line options and actions
-       try: myaction = parseArgs(myoptions)
-       # filter exception to know what message to display
-       except ParseArgsException, e:
-               if e.value == 'help':
-                       printUsage(help='all')
-                       sys.exit(0)
-               elif e.value[:5] == 'help-':
-                       printUsage(help=e.value[5:])
-                       sys.exit(0)
-               elif e.value == 'version':
-                       printVersion()
-                       sys.exit(0)
-               else: 
-                       printUsage(e.value)
-                       sys.exit(2)
-       # parse the exclusion file
-       if not 'exclude-file' in myoptions:
-               my_exclude_file = "/etc/%s/%s.exclude" % (__productname__ , myaction)
-               if os.path.isfile(my_exclude_file):
-                       myoptions['exclude-file'] = my_exclude_file
-       if 'exclude-file' in myoptions:
-               try: exclude_dict = parseExcludeFile(myoptions['exclude-file'])
-               except ParseExcludeFileException, e:
-                       eerror(e, myoptions['nocolor'])
-                       eerror("Invalid exclusion file: %s" % myoptions['exclude-file'], \
-                                       myoptions['nocolor'])
-                       eerror("See format of this file in `man %s`" % __productname__, \
-                                       myoptions['nocolor'])   
-                       sys.exit(1)
-       else: exclude_dict={}
-       # security check for non-pretend mode
-       if not myoptions['pretend'] and portage.secpass == 0:
-               eerror("Permission denied: you must be root or belong to the portage group.", \
-                      myoptions['nocolor'])
-               sys.exit(1)
-       # execute action
-       doAction(myaction, myoptions, exclude_dict=exclude_dict)
-
-
-###############################################################################
-# actually call main() if launched as a script
-if __name__ == "__main__":
-       try: main()
-       except KeyboardInterrupt:
-               print "Aborted."
-               sys.exit(130)
-       sys.exit(0)
-