aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/eclean/eclean')
-rw-r--r--src/eclean/eclean838
1 files changed, 838 insertions, 0 deletions
diff --git a/src/eclean/eclean b/src/eclean/eclean
new file mode 100644
index 0000000..55cc2a7
--- /dev/null
+++ b/src/eclean/eclean
@@ -0,0 +1,838 @@
+#!/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)
+