aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'pym/gentoolkit/eclean')
-rw-r--r--pym/gentoolkit/eclean/__init__.py4
-rw-r--r--pym/gentoolkit/eclean/clean.py149
-rw-r--r--pym/gentoolkit/eclean/cli.py500
-rw-r--r--pym/gentoolkit/eclean/exclude.py262
-rw-r--r--pym/gentoolkit/eclean/output.py179
-rw-r--r--pym/gentoolkit/eclean/pkgindex.py89
-rw-r--r--pym/gentoolkit/eclean/search.py520
7 files changed, 1703 insertions, 0 deletions
diff --git a/pym/gentoolkit/eclean/__init__.py b/pym/gentoolkit/eclean/__init__.py
new file mode 100644
index 0000000..edf9385
--- /dev/null
+++ b/pym/gentoolkit/eclean/__init__.py
@@ -0,0 +1,4 @@
+#!/usr/bin/python
+#
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
diff --git a/pym/gentoolkit/eclean/clean.py b/pym/gentoolkit/eclean/clean.py
new file mode 100644
index 0000000..9f6d597
--- /dev/null
+++ b/pym/gentoolkit/eclean/clean.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+
+from __future__ import print_function
+
+
+import sys
+
+from portage import os
+import gentoolkit.pprinter as pp
+from gentoolkit.eclean.pkgindex import PkgIndex
+
+
+class CleanUp(object):
+ """Performs all cleaning actions to distfiles or package directories.
+
+ @param controller: a progress output/user interaction controller function
+ which returns a Boolean to control file deletion
+ or bypassing/ignoring
+ """
+
+ def __init__(self, controller):
+ self.controller = controller
+
+ def clean_dist(self, clean_dict):
+ """Calculate size of each entry for display, prompt user if needed,
+ delete files if approved and return the total size of files that
+ have been deleted.
+
+ @param clean_dict: dictionary of {'display name':[list of files]}
+
+ @rtype: int
+ @return: total size that was cleaned
+ """
+ file_type = 'file'
+ clean_keys = self._sort_keys(clean_dict)
+ clean_size = 0
+ # clean all entries one by one
+ for key in clean_keys:
+ clean_size += self._clean_files(clean_dict[key], key, file_type)
+ # return total size of deleted or to delete files
+ return clean_size
+
+ def clean_pkgs(self, clean_dict, pkgdir):
+ """Calculate size of each entry for display, prompt user if needed,
+ delete files if approved and return the total size of files that
+ have been deleted.
+
+ @param clean_dict: dictionary of {'display name':[list of files]}
+ @param metadata: package index of type portage.getbinpkg.PackageIndex()
+ @param pkgdir: path to the package directory to be cleaned
+
+ @rtype: int
+ @return: total size that was cleaned
+ """
+ file_type = 'binary package'
+ clean_keys = self._sort_keys(clean_dict)
+ clean_size = 0
+ # clean all entries one by one
+ for key in clean_keys:
+ clean_size += self._clean_files(clean_dict[key], key, file_type)
+
+ # run 'emaint --fix' here
+ if clean_size:
+ index_control = PkgIndex(self.controller)
+ # emaint is not yet importable so call it
+ # print a blank line here for separation
+ print()
+ clean_size += index_control.call_emaint()
+ # return total size of deleted or to delete files
+ return clean_size
+
+
+ def pretend_clean(self, clean_dict):
+ """Shortcut function that calculates total space savings
+ for the files in clean_dict.
+
+ @param clean_dict: dictionary of {'display name':[list of files]}
+ @rtype: integer
+ @return: total size that would be cleaned
+ """
+ file_type = 'file'
+ clean_keys = self._sort_keys(clean_dict)
+ clean_size = 0
+ # tally all entries one by one
+ for key in clean_keys:
+ key_size = self._get_size(clean_dict[key])
+ self.controller(key_size, key, clean_dict[key], file_type)
+ clean_size += key_size
+ return clean_size
+
+ def _get_size(self, key):
+ """Determine the total size for an entry (may be several files)."""
+ key_size = 0
+ for file_ in key:
+ #print file_
+ # get total size for an entry (may be several files, and
+ # links don't count
+ # ...get its statinfo
+ try:
+ statinfo = os.stat(file_)
+ if statinfo.st_nlink == 1:
+ key_size += statinfo.st_size
+ except EnvironmentError as er:
+ print( pp.error(
+ "Could not get stat info for:" + file_), file=sys.stderr)
+ print( pp.error("Error: %s" %str(er)), file=sys.stderr)
+ return key_size
+
+ def _sort_keys(self, clean_dict):
+ """Returns a list of sorted dictionary keys."""
+ # sorting helps reading
+ clean_keys = sorted(clean_dict)
+ return clean_keys
+
+ def _clean_files(self, files, key, file_type):
+ """File removal function."""
+ clean_size = 0
+ for file_ in files:
+ #print file_, type(file_)
+ # ...get its statinfo
+ try:
+ statinfo = os.stat(file_)
+ except EnvironmentError as er:
+ print( pp.error(
+ "Could not get stat info for:" + file_), file=sys.stderr)
+ print( pp.error(
+ "Error: %s" %str(er)), file=sys.stderr)
+ if self.controller(statinfo.st_size, key, file_, file_type):
+ # ... try to delete it.
+ try:
+ os.unlink(file_)
+ # only count size if successfully deleted and not a link
+ if statinfo.st_nlink == 1:
+ clean_size += statinfo.st_size
+ except EnvironmentError as er:
+ print( pp.error("Could not delete "+file_), file=sys.stderr)
+ print( pp.error("Error: %s" %str(er)), file=sys.stderr)
+ return clean_size
+
+
+
+
+
+
+
diff --git a/pym/gentoolkit/eclean/cli.py b/pym/gentoolkit/eclean/cli.py
new file mode 100644
index 0000000..6a507ef
--- /dev/null
+++ b/pym/gentoolkit/eclean/cli.py
@@ -0,0 +1,500 @@
+#!/usr/bin/python
+
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+
+from __future__ import print_function
+
+
+__author__ = "Thomas de Grenier de Latour (tgl), " + \
+ "modular re-write by: Brian Dolbec (dol-sen)"
+__email__ = "degrenier@easyconnect.fr, " + \
+ "brian.dolbec@gmail.com"
+__version__ = "svn"
+__productname__ = "eclean"
+__description__ = "A cleaning tool for Gentoo distfiles and binaries."
+
+
+import sys
+import re
+import time
+import getopt
+
+import portage
+from portage import os
+from portage.output import white, yellow, turquoise, green, teal, red
+
+import gentoolkit.pprinter as pp
+from gentoolkit.eclean.search import (DistfilesSearch,
+ findPackages, port_settings, pkgdir)
+from gentoolkit.eclean.exclude import (parseExcludeFile,
+ ParseExcludeFileException)
+from gentoolkit.eclean.clean import CleanUp
+from gentoolkit.eclean.output import OutputControl
+#from gentoolkit.eclean.dbapi import Dbapi
+
+def printVersion():
+ """Output the version info."""
+ print( "%s (%s) - %s" \
+ % (__productname__, __version__, __description__))
+ print()
+ print("Author: %s <%s>" % (__author__,__email__))
+ print("Copyright 2003-2009 Gentoo Foundation")
+ print("Distributed under the terms of the GNU General Public License v2")
+
+
+def printUsage(_error=None, help=None):
+ """Print help message. May also print partial help to stderr if an
+ error from {'options','actions'} is specified."""
+
+ 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':
+ print( pp.error("Wrong time specification"), file=out)
+ print( "Time specification should be an integer followed by a"+
+ " single letter unit.", file=out)
+ print( "Available units are: y (years), m (months), w (weeks), "+
+ "d (days) and h (hours).", file=out)
+ print( "For instance: \"1y\" is \"one year\", \"2w\" is \"two"+
+ " weeks\", etc. ", file=out)
+ return
+ if _error == 'size':
+ print( pp.error("Wrong size specification"), file=out)
+ print( "Size specification should be an integer followed by a"+
+ " single letter unit.", file=out)
+ print( "Available units are: G, M, K and B.", file=out)
+ print("For instance: \"10M\" is \"ten megabytes\", \"200K\" "+
+ "is \"two hundreds kilobytes\", etc.", file=out)
+ return
+ if _error in ('global-options', 'packages-options', 'distfiles-options', \
+ 'merged-packages-options', 'merged-distfiles-options',):
+ print( pp.error("Wrong option on command line."), file=out)
+ print( file=out)
+ elif _error == 'actions':
+ print( pp.error("Wrong or missing action name on command line."), file=out)
+ print( file=out)
+ print( white("Usage:"), file=out)
+ if _error in ('actions','global-options', 'packages-options', \
+ 'distfiles-options') or help == 'all':
+ print( " "+turquoise(__productname__),
+ yellow("[global-option] ..."),
+ green("<action>"),
+ yellow("[action-option] ..."), file=out)
+ if _error == 'merged-distfiles-options' or help in ('all','distfiles'):
+ print( " "+turquoise(__productname__+'-dist'),
+ yellow("[global-option, distfiles-option] ..."), file=out)
+ if _error == 'merged-packages-options' or help in ('all','packages'):
+ print( " "+turquoise(__productname__+'-pkg'),
+ yellow("[global-option, packages-option] ..."), file=out)
+ if _error in ('global-options', 'actions'):
+ print( " "+turquoise(__productname__),
+ yellow("[--help, --version]"), file=out)
+ if help == 'all':
+ print( " "+turquoise(__productname__+"(-dist,-pkg)"),
+ yellow("[--help, --version]"), file=out)
+ if _error == 'merged-packages-options' or help == 'packages':
+ print( " "+turquoise(__productname__+'-pkg'),
+ yellow("[--help, --version]"), file=out)
+ if _error == 'merged-distfiles-options' or help == 'distfiles':
+ print( " "+turquoise(__productname__+'-dist'),
+ yellow("[--help, --version]"), file=out)
+ print(file=out)
+ if _error in ('global-options', 'merged-packages-options', \
+ 'merged-distfiles-options') or help:
+ print( "Available global", yellow("options")+":", file=out)
+ print( yellow(" -C, --nocolor")+
+ " - turn off colors on output", file=out)
+ print( yellow(" -d, --destructive")+
+ " - only keep the minimum for a reinstallation", file=out)
+ print( yellow(" -e, --exclude-file=<path>")+
+ " - path to the exclusion file", file=out)
+ print( yellow(" -i, --interactive")+
+ " - ask confirmation before deletions", file=out)
+ print( yellow(" -n, --package-names")+
+ " - protect all versions (when --destructive)", file=out)
+ print( yellow(" -p, --pretend")+
+ " - only display what would be cleaned", file=out)
+ print( yellow(" -q, --quiet")+
+ " - be as quiet as possible", file=out)
+ print( yellow(" -t, --time-limit=<time>")+
+ " - don't delete files modified since "+yellow("<time>"), file=out)
+ print( " "+yellow("<time>"), "is a duration: \"1y\" is"+
+ " \"one year\", \"2w\" is \"two weeks\", etc. ", file=out)
+ print( " "+"Units are: y (years), m (months), w (weeks), "+
+ "d (days) and h (hours).", file=out)
+ print( yellow(" -h, --help")+ \
+ " - display the help screen", file=out)
+ print( yellow(" -V, --version")+
+ " - display version info", file=out)
+ print( file=out)
+ if _error == 'actions' or help == 'all':
+ print( "Available", green("actions")+":", file=out)
+ print( green(" packages")+
+ " - clean outdated binary packages from PKGDIR", file=out)
+ print( green(" distfiles")+
+ " - clean outdated packages sources files from DISTDIR", file=out)
+ print( file=out)
+ if _error in ('packages-options','merged-packages-options') \
+ or help in ('all','packages'):
+ print( "Available", yellow("options"),"for the",
+ green("packages"),"action:", file=out)
+ print( yellow(" NONE :)"), file=out)
+ print( file=out)
+ if _error in ('distfiles-options', 'merged-distfiles-options') \
+ or help in ('all','distfiles'):
+ print("Available", yellow("options"),"for the",
+ green("distfiles"),"action:", file=out)
+ print( yellow(" -f, --fetch-restricted")+
+ " - protect fetch-restricted files (when --destructive)", file=out)
+ print( yellow(" -s, --size-limit=<size>")+
+ " - don't delete distfiles bigger than "+yellow("<size>"), file=out)
+ print( " "+yellow("<size>"), "is a size specification: "+
+ "\"10M\" is \"ten megabytes\", \"200K\" is", file=out)
+ print( " "+"\"two hundreds kilobytes\", etc. Units are: "+
+ "G, M, K and B.", file=out)
+ print( file=out)
+ print( "More detailed instruction can be found in",
+ turquoise("`man %s`" % __productname__), file=out)
+
+
+class ParseArgsException(Exception):
+ """For parseArgs() -> main() communications."""
+ def __init__(self, value):
+ self.value = value # sdfgsdfsdfsd
+ def __str__(self):
+ return repr(self.value)
+
+
+def parseSize(size):
+ """Convert a file size "Xu" ("X" is an integer, and "u" in
+ [G,M,K,B]) into an integer (file size in Bytes).
+
+ @raise ParseArgsException: in case of failure
+ """
+ units = {
+ 'G': (1024**3),
+ 'M': (1024**2),
+ 'K': 1024,
+ 'B': 1
+ }
+ try:
+ match = re.match(r"^(?P<value>\d+)(?P<unit>[GMKBgmkb])?$",size)
+ size = int(match.group('value'))
+ if match.group('unit'):
+ size *= units[match.group('unit').capitalize()]
+ except:
+ raise ParseArgsException('size')
+ return size
+
+
+def parseTime(timespec):
+ """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?).
+ """
+ units = {'H' : (60 * 60)}
+ units['D'] = units['H'] * 24
+ units['W'] = units['D'] * 7
+ units['M'] = units['D'] * 30
+ units['Y'] = units['D'] * 365
+ try:
+ # parse the time specification
+ match = re.match(r"^(?P<value>\d+)(?P<unit>[YMWDHymwdh])?$",timespec)
+ value = int(match.group('value'))
+ if not match.group('unit'): unit = 'D'
+ else: unit = match.group('unit').capitalize()
+ except:
+ raise ParseArgsException('time')
+ return time.time() - (value * units[unit])
+
+
+def parseArgs(options={}):
+ """Parse the command line arguments. Raise exceptions on
+ errors or non-action modes (help/version). Returns an action, and affect
+ the options dict.
+ """
+
+ def optionSwitch(option,opts,action=None):
+ """local function for interpreting command line options
+ and setting options accordingly"""
+ 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"):
+ options['nocolor'] = True
+ pp.output.nocolor()
+ elif o in ("-d", "--destructive"):
+ options['destructive'] = True
+ elif o in ("-D", "--deprecated"):
+ options['deprecated'] = True
+ elif o in ("-i", "--interactive") and not options['pretend']:
+ options['interactive'] = True
+ elif o in ("-p", "--pretend"):
+ options['pretend'] = True
+ options['interactive'] = False
+ elif o in ("-q", "--quiet"):
+ options['quiet'] = True
+ options['verbose'] = False
+ elif o in ("-t", "--time-limit"):
+ options['time-limit'] = parseTime(a)
+ elif o in ("-e", "--exclude-file"):
+ options['exclude-file'] = a
+ elif o in ("-n", "--package-names"):
+ options['package-names'] = True
+ elif o in ("-f", "--fetch-restricted"):
+ options['fetch-restricted'] = True
+ elif o in ("-s", "--size-limit"):
+ options['size-limit'] = parseSize(a)
+ elif o in ("-v", "--verbose") and not options['quiet']:
+ options['verbose'] = True
+ else:
+ return_code = False
+ # sanity check of --destructive only options:
+ for opt in ('fetch-restricted', 'package-names'):
+ if (not options['destructive']) and options[opt]:
+ if not options['quiet']:
+ print( pp.error(
+ "--%s only makes sense in --destructive mode." % opt), file=sys.stderr)
+ options[opt] = False
+ return return_code
+
+ # here are the different allowed command line options (getopt args)
+ getopt_options = {'short':{}, 'long':{}}
+ getopt_options['short']['global'] = "CdDipqe:t:nhVv"
+ getopt_options['long']['global'] = ["nocolor", "destructive",
+ "deprecated", "interactive", "pretend", "quiet", "exclude-file=",
+ "time-limit=", "package-names", "help", "version", "verbose"]
+ 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()
+ options['interactive'] = False
+ options['pretend'] = False
+ options['quiet'] = False
+ options['accept_all'] = False
+ options['destructive'] = False
+ options['deprecated'] = False
+ options['time-limit'] = 0
+ options['package-names'] = False
+ options['fetch-restricted'] = False
+ options['size-limit'] = 0
+ options['verbose'] = False
+ # if called by a well-named symlink, set the acction accordingly:
+ action = None
+ # temp print line to ensure it is the svn/branch code running, etc..
+ #print( "###### svn/branch/gentoolkit_eclean ####### ==> ", os.path.basename(sys.argv[0]))
+ if os.path.basename(sys.argv[0]) in \
+ (__productname__+'-pkg', __productname__+'-packages'):
+ action = 'packages'
+ elif os.path.basename(sys.argv[0]) in \
+ (__productname__+'-dist', __productname__+'-distfiles'):
+ action = 'distfiles'
+ # prepare for the first getopt
+ if action:
+ short_opts = getopt_options['short']['global'] \
+ + getopt_options['short'][action]
+ long_opts = getopt_options['long']['global'] \
+ + getopt_options['long'][action]
+ opts_mode = 'merged-'+action
+ 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 options accordingly
+ optionSwitch(options,opts,action=action)
+ # if action was already set, there should be no more args
+ if action and len(args):
+ raise ParseArgsException(opts_mode+'-options')
+ # if action was set, there is nothing left to do
+ if action:
+ return action
+ # 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')
+ action = args.pop(0)
+ # parse the action specific options
+ try:
+ opts, args = getopt.getopt(args, \
+ getopt_options['short'][action], \
+ getopt_options['long'][action])
+ except:
+ raise ParseArgsException(action+'-options')
+ # set options again, for action-specific options
+ optionSwitch(options,opts,action=action)
+ # any remaning args? Then die!
+ if len(args):
+ raise ParseArgsException(action+'-options')
+ # returns the action. Options dictionary is modified by side-effect.
+ return action
+
+
+def doAction(action,options,exclude={}, output=None):
+ """doAction: execute one action, ie display a few message, call the right
+ find* function, and then call doCleanup with its result."""
+ # define vocabulary for the output
+ if action == 'packages':
+ files_type = "binary packages"
+ else:
+ files_type = "distfiles"
+ saved = {}
+ deprecated = {}
+ # find files to delete, depending on the action
+ if not options['quiet']:
+ output.einfo("Building file list for "+action+" cleaning...")
+ if action == 'packages':
+ clean_me = findPackages(
+ options,
+ exclude=exclude,
+ destructive=options['destructive'],
+ package_names=options['package-names'],
+ time_limit=options['time-limit'],
+ pkgdir=pkgdir,
+ #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
+ #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
+ )
+ else:
+ # accept defaults
+ engine = DistfilesSearch(output=options['verbose-output'],
+ #portdb=Dbapi(portage.db[portage.root]["porttree"].dbapi),
+ #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
+ )
+ clean_me, saved, deprecated = engine.findDistfiles(
+ exclude=exclude,
+ destructive=options['destructive'],
+ fetch_restricted=options['fetch-restricted'],
+ package_names=options['package-names'],
+ time_limit=options['time-limit'],
+ size_limit=options['size-limit'],
+ deprecate = options['deprecated']
+ )
+ # actually clean files if something was found
+ if clean_me:
+ # verbose pretend message
+ if options['pretend'] and not options['quiet']:
+ output.einfo("Here are the "+files_type+" that would be deleted:")
+ # verbose non-pretend message
+ elif not options['quiet']:
+ output.einfo("Cleaning " + files_type +"...")
+ # do the cleanup, and get size of deleted files
+ cleaner = CleanUp( output.progress_controller)
+ if options['pretend']:
+ clean_size = cleaner.pretend_clean(clean_me)
+ elif action in ['distfiles']:
+ clean_size = cleaner.clean_dist(clean_me)
+ elif action in ['packages']:
+ clean_size = cleaner.clean_pkgs(clean_me,
+ pkgdir)
+ # vocabulary for final message
+ if options['pretend']:
+ verb = "would be"
+ else:
+ verb = "were"
+ # display freed space
+ if not options['quiet']:
+ output.total('normal', clean_size, len(clean_me), verb, action)
+ # nothing was found, return
+ elif not options['quiet']:
+ output.einfo("Your "+action+" directory was already clean.")
+ if saved and not options['quiet']:
+ print()
+ print( (pp.emph(" The folowing ") + yellow("Deprecated") +
+ pp.emph(" files were saved from cleaning due to exclusion file entries")))
+ output.set_colors('deprecated')
+ clean_size = cleaner.pretend_clean(saved)
+ output.total('deprecated', clean_size, len(saved), verb, action)
+ if deprecated and not options['quiet']:
+ print()
+ print( (pp.emph(" The folowing ") + yellow("Deprecated") +
+ pp.emph(" installed packages were found")))
+ output.set_colors('deprecated')
+ output.list_pkgs(deprecated)
+
+
+def main():
+ """Parse command line and execute all actions."""
+ # set default options
+ options = {}
+ options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
+ or not sys.stdout.isatty())
+ if options['nocolor']:
+ pp.output.nocolor()
+ # parse command line options and actions
+ try:
+ action = parseArgs(options)
+ # filter exception to know what message to display
+ except ParseArgsException as 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)
+ output = OutputControl(options)
+ options['verbose-output'] = lambda x: None
+ if not options['quiet']:
+ if options['verbose']:
+ options['verbose-output'] = output.einfo
+ # parse the exclusion file
+ if 'exclude-file' in options:
+ try:
+ exclude = parseExcludeFile(options['exclude-file'],
+ options['verbose-output'])
+ except ParseExcludeFileException as e:
+ print( pp.error(str(e)), file=sys.stderr)
+ print( pp.error(
+ "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
+ print( pp.error(
+ "See format of this file in `man %s`" % __productname__), file=sys.stderr)
+ sys.exit(1)
+ else:
+ exclude_file = "/etc/%s/%s.exclude" % (__productname__ , action)
+ if os.path.isfile(exclude_file):
+ options['exclude-file'] = exclude_file
+ exclude={}
+ # security check for non-pretend mode
+ if not options['pretend'] and portage.secpass == 0:
+ print( pp.error(
+ "Permission denied: you must be root or belong to " +
+ "the portage group."), file=sys.stderr)
+ sys.exit(1)
+ # execute action
+ doAction(action, options, exclude=exclude,
+ output=output)
+
+
+if __name__ == "__main__":
+ """actually call main() if launched as a script"""
+ try:
+ main()
+ except KeyboardInterrupt:
+ print( "Aborted.")
+ sys.exit(130)
+ sys.exit(0)
diff --git a/pym/gentoolkit/eclean/exclude.py b/pym/gentoolkit/eclean/exclude.py
new file mode 100644
index 0000000..74a982a
--- /dev/null
+++ b/pym/gentoolkit/eclean/exclude.py
@@ -0,0 +1,262 @@
+#!/usr/bin/python
+
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+
+from __future__ import print_function
+
+
+import sys
+import re
+import portage
+
+from portage import os
+from gentoolkit.pprinter import warn
+
+# Misc. shortcuts to some portage stuff:
+listdir = portage.listdir
+
+FILENAME_RE = [re.compile(r'(?P<pkgname>[-a-zA-z0-9\+]+)(?P<ver>-\d+\S+)'),
+ re.compile(r'(?P<pkgname>[-a-zA-z]+)(?P<ver>_\d+\S+)'),
+ re.compile(r'(?P<pkgname>[-a-zA-z_]+)(?P<ver>\d\d+\S+)'),
+ re.compile(r'(?P<pkgname>[-a-zA-z0-9_]+)(?P<ver>-default\S+)'),
+ re.compile(r'(?P<pkgname>[-a-zA-z0-9]+)(?P<ver>_\d\S+)'),
+ re.compile(r'(?P<pkgname>[-a-zA-z0-9\+\.]+)(?P<ver>-\d+\S+)'),
+ re.compile(r'(?P<pkgname>[-a-zA-z0-9\+\.]+)(?P<ver>.\d+\S+)')]
+
+debug_modules = []
+
+def dprint(module, message):
+ if module in debug_modules:
+ print(message)
+
+def isValidCP(cp):
+ """Check whether 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...
+
+ @param cp: catageory/package string
+ @rtype: bool
+ """
+
+ if not '/' in cp:
+ return False
+ try:
+ portage.cpv_getkey(cp+"-0")
+ except:
+ return False
+ else:
+ return True
+
+
+class ParseExcludeFileException(Exception):
+ """For parseExcludeFile() -> main() communication.
+
+ @param value: Error message string
+ """
+ def __init__(self, value):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
+
+
+def parseExcludeFile(filepath, output):
+ """Parses an exclusion file.
+
+ @param filepath: file containing the list of cat/pkg's to exclude
+ @param output: --verbose enabled output method or "lambda x: None"
+
+ @rtype: dict
+ @return: an exclusion dict
+ @raise ParseExcludeFileException: in case of fatal error
+ """
+
+ exclude = {
+ 'categories': {},
+ 'packages': {},
+ 'anti-packages': {},
+ 'filenames': {}
+ }
+ output("Parsing Exclude file: " + filepath)
+ try:
+ file_ = open(filepath,"r")
+ except IOError:
+ raise ParseExcludeFileException("Could not open exclusion file: " +
+ filepath)
+ 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_]+)$')
+ # used to output the line number for exception error reporting
+ linenum = 0
+ for line in filecontents:
+ # need to increment it here due to continue statements.
+ linenum += 1
+ line = line.strip()
+ if not len(line): # skip blank a line
+ continue
+ if line[0] == '#': # skip a comment line
+ continue
+ #print( "parseExcludeFile: line=", line)
+ try: # category matching
+ cat = cat_re.match(line).group('cat')
+ #print( "parseExcludeFile: found cat=", cat)
+ except:
+ pass
+ else:
+ if not cat in portage.settings.categories:
+ raise ParseExcludeFileException("Invalid category: "+cat +
+ " @line # " + str(linenum))
+ exclude['categories'][cat] = None
+ continue
+ dict_key = 'packages'
+ if line[0] == '!': # reverses category setting
+ dict_key = 'anti-packages'
+ line = line[1:]
+ try: # cat/pkg matching
+ cp = cp_re.match(line).group('cp')
+ #print( "parseExcludeFile: found cp=", cp)
+ if isValidCP(cp):
+ exclude[dict_key][cp] = None
+ continue
+ else:
+ raise ParseExcludeFileException("Invalid cat/pkg: "+cp +
+ " @line # " + str(linenum))
+ except:
+ pass
+ #raise ParseExcludeFileException("Invalid line: "+line)
+ try: # filename matching.
+ exclude['filenames'][line] = re.compile(line)
+ #print( "parseExcludeFile: found filenames", line)
+ except:
+ try:
+ exclude['filenames'][line] = re.compile(re.escape(line))
+ #print( "parseExcludeFile: found escaped filenames", line)
+ except:
+ raise ParseExcludeFileException("Invalid file name/regular " +
+ "expression: @line # " + str(linenum) + " line=" +line)
+ output("Exclude file parsed. Found " +
+ "%d categories, %d packages, %d anti-packages %d filenames"
+ %(len(exclude['categories']), len(exclude['packages']),
+ len(exclude['anti-packages']), len(exclude['filenames'])))
+ #print()
+ #print( "parseExcludeFile: final exclude_dict = ", exclude)
+ #print()
+ return exclude
+
+def cp_all(categories):
+ """temp function until the new portdb.cp_all([cat,...])
+ behaviour is fully available.
+
+ @param categories: list of categories to get all packages for
+ eg. ['app-portage', 'sys-apps',...]
+ @rtype: list of cat/pkg's ['foo/bar', 'foo/baz']
+ """
+ try:
+ cps = portage.portdb.cp_all(categories)
+ message = "Deprication Warning: eclean.exclude.cp_all()\n" + \
+ "New portage functionality is available " +\
+ "Please migrate code permanently"
+ print( warn(message), file=sys.stderr)
+ except: # new behaviour not available
+ #~ message = "Exception: eclean.exclude.cp_all() " +\
+ #~ "new portdb.cp_all() behavior not found. using fallback code"
+ #~ print( warn(message), file=sys.stderr)
+ cps = []
+ # XXX: i smell an access to something which is really out of API...
+ _pkg_dir_name_re = re.compile(r'^\w[-+\w]*$')
+ for tree in portage.portdb.porttrees:
+ for cat in categories:
+ for pkg in listdir(os.path.join(tree,cat),
+ EmptyOnError=1, ignorecvs=1, dirsonly=1):
+ if not _pkg_dir_name_re.match(pkg) or pkg == "CVS":
+ continue
+ cps.append(cat+'/'+pkg)
+ #print( "cp_all: new cps list=", cps)
+ return cps
+
+def exclDictExpand(exclude):
+ """Returns a dictionary of all CP/CPV from porttree which match
+ the exclusion dictionary.
+ """
+ d = {}
+ if 'categories' in exclude:
+ # replace the following cp_all call with
+ # portage.portdb.cp_all([cat1, cat2])
+ # when it is available in all portage versions.
+ cps = cp_all(exclude['categories'])
+ for cp in cps:
+ d[cp] = None
+ if 'packages' in exclude:
+ for cp in exclude['packages']:
+ d[cp] = None
+ if 'anti-packages' in exclude:
+ for cp in exclude['anti-packages']:
+ if cp in d:
+ del d[cp]
+ return d
+
+def exclDictMatchCP(exclude,pkg):
+ """Checks whether a CP matches the exclusion rules."""
+ if 'anti-packages' in exclude and pkg in exclude['anti-packages']:
+ return False
+ if 'packages' in exclude and pkg in exclude['packages']:
+ return True
+ cat = pkg.split('/')[0]
+ if 'categories' in exclude and cat in exclude['categories']:
+ return True
+ return False
+
+def exclDictExpandPkgname(exclude):
+ """Returns a set of all pkgnames from porttree which match
+ the exclusion dictionary.
+ """
+ p = set()
+ if 'categories' in exclude:
+ # replace the following cp_all call with
+ # portage.portdb.cp_all([cat1, cat2])
+ # when it is available in all portage versions.
+ cps = cp_all(exclude['categories'])
+ for cp in cps:
+ pkgname = cp.split('/')[1]
+ p.add(pkgname)
+ if 'packages' in exclude:
+ pkgname = cp.split('/')[1]
+ p.add(pkgname)
+ if 'anti-packages' in exclude:
+ for cp in exclude['anti-packages']:
+ if cp in p:
+ p.remove(cp)
+ return p
+
+
+def exclMatchFilename(exclude_names, filename):
+ """Attempts to split the package name out of a filename
+ and then checks if it matches any exclusion rules.
+
+ This is intended to be run on the cleaning list after all
+ normal checks and removal of protected files. This will reduce
+ the number of files to perform this last minute check on
+
+ @param exclude_names: a set of pkgnames to exlcude
+ @param filename:
+
+ @rtype: bool
+ """
+ found = False
+ index = 0
+ while not found and index < len(FILENAME_RE):
+ found = FILENAME_RE[index].match(filename)
+ index += 1
+ if not found:
+ dprint( "exclude", "exclMatchFilename: filename: " +\
+ "%s, Could not determine package name" %filename)
+ return False
+ pkgname = found.group('pkgname')
+ dprint("exclude", "exclMatchFilename: found pkgname = " +
+ "%s, %s, %d, %s" %(pkgname, str(pkgname in exclude_names),
+ index-1, filename))
+ return (pkgname in exclude_names)
+
diff --git a/pym/gentoolkit/eclean/output.py b/pym/gentoolkit/eclean/output.py
new file mode 100644
index 0000000..670e67c
--- /dev/null
+++ b/pym/gentoolkit/eclean/output.py
@@ -0,0 +1,179 @@
+#!/usr/bin/python
+
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+
+from __future__ import print_function
+
+
+import sys
+import portage
+from portage.output import *
+from gentoolkit.pprinter import cpv, number, emph
+
+
+class OutputControl(object):
+ """Outputs data according to predetermined options and handles any user
+ interaction.
+
+ @param options: dictionary of boolean options as determined in cli.py
+ used here: interactive, pretend, quiet, accept_all, nocolor.
+ """
+
+ def __init__(self, options):
+ if not options:
+ # set some defaults
+ self.options['interactive'] = False
+ self.options['pretend'] = True
+ self.options['quiet'] = False
+ self.options['accept_all'] = False
+ self.options['nocolor'] = False
+ else:
+ self.options = options
+ self.set_colors("normal")
+
+ def set_colors(self, mode):
+ """Sets the colors for the progress_controller
+ and prettysize output
+
+ @param mode: string, 1 of ["normal", "deprecated"]
+ """
+ if mode == "normal":
+ self.pkg_color = cpv # green
+ self.numbers = number # turquoise
+ self.brace = blue
+ elif mode == "deprecated":
+ self.pkg_color = yellow
+ self.numbers = teal # darkgreen
+ self.brace = blue
+
+ def einfo(self, message=""):
+ """Display an info message depending on a color mode.
+
+ @param message: text string to display
+
+ @outputs to stdout.
+ """
+ if not self.options['nocolor']:
+ prefix = " "+green('*')
+ else:
+ prefix = ">>>"
+ print(prefix,message)
+
+ def eprompt(self, message):
+ """Display a user question depending on a color mode.
+
+ @param message: text string to display
+
+ @output to stdout
+ """
+ if not self.options['nocolor']:
+ prefix = " "+red('>')+" "
+ else:
+ prefix = "??? "
+ sys.stdout.write(prefix+message)
+ sys.stdout.flush()
+
+ def prettySize(self, size, justify=False, color=None):
+ """int -> byte/kilo/mega/giga converter. Optionally
+ justify the result. Output is a string.
+
+ @param size: integer
+ @param justify: optional boolean, defaults to False
+ @param color: optional color, defaults to green
+ as defined in portage.output
+
+ @returns a formatted and (escape sequenced)
+ colorized text string
+ """
+ if color == None:
+ color = self.numbers
+ units = [" G"," M"," K"," B"]
+ approx = 0
+ while len(units) and size >= 1000:
+ approx = 1
+ size = size / 1024.
+ units.pop()
+ sizestr = '%d'% size + units[-1]
+ if justify:
+ sizestr = " " + self.brace("[ ") + \
+ color(sizestr.rjust(7)) + self.brace(" ]")
+ return sizestr
+
+ def yesNoAllPrompt(self, message="Do you want to proceed?"):
+ """Print a prompt until user answer in yes/no/all. Return a
+ boolean for answer, and also may affect the 'accept_all' option.
+
+ @param message: optional different input string from the default
+ message of: "Do you want to proceed?"
+ @outputs to stdout
+ @modifies class var options['accept_all']
+ @rtype: bool
+ """
+ user_string="xxx"
+ while not user_string.lower() in ["","y","n","a","yes","no","all"]:
+ self.eprompt(message+" [Y/n/a]: ")
+ user_string = sys.stdin.readline().rstrip('\n')
+ user_string = user_string.strip()
+ if user_string.lower() in ["a","all"]:
+ self.options['accept_all'] = True
+ answer = user_string.lower() in ["","y","a","yes","all"]
+ return answer
+
+ def progress_controller(self, size, key, clean_list, file_type):
+ """Callback function for doCleanup. It outputs data according to the
+ options configured.
+ Alternatively it handles user interaction for decisions that are
+ required.
+
+ @param size: Integer of the file(s) size
+ @param key: the filename/pkgname currently being processed
+ @param clean_list: list of files being processed.
+ """
+ if not self.options['quiet']:
+ # pretty print mode
+ print(self.prettySize(size,True), self.pkg_color(key))
+ elif self.options['pretend'] or self.options['interactive']:
+ # file list mode
+ for file_ in clean_list:
+ print(file_)
+ if self.options['pretend']:
+ return False
+ elif not self.options['interactive'] \
+ or self.options['accept_all'] \
+ or self.yesNoAllPrompt("Do you want to delete this " + file_type + "?"):
+ return True
+ return False
+
+ def total(self, mode, size, num_files, verb, action):
+ """outputs the formatted totals to stdout
+
+ @param mode: sets color and message. 1 of ['normal', 'deprecated']
+ @param size: total space savings
+ @param num_files: total number of files
+ @param verb: string eg. 1 of ["would be", "has been"]
+ @param action: string eg 1 of ['distfiles', 'packages']
+ """
+ self.set_colors(mode)
+ if mode =="normal":
+ message="Total space from "+red(str(num_files))+" files "+\
+ verb+" freed in the " + action + " directory"
+ print( " ===========")
+ print( self.prettySize(size, True, red), message)
+ elif mode == "deprecated":
+ message = "Total space from "+red(str(num_files))+" package files\n"+\
+ " Re-run the last command with the -D " +\
+ "option to clean them as well"
+ print( " ===========")
+ print( self.prettySize(size, True, red), message)
+
+ def list_pkgs(self, pkgs):
+ """outputs the packages to stdout
+
+ @param pkgs: dict. of {cat/pkg-ver: src_uri,}
+ """
+ indent = ' ' * 12
+ for key in pkgs:
+ print( indent,self.pkg_color(key))
+ print()
diff --git a/pym/gentoolkit/eclean/pkgindex.py b/pym/gentoolkit/eclean/pkgindex.py
new file mode 100644
index 0000000..f9d9f3c
--- /dev/null
+++ b/pym/gentoolkit/eclean/pkgindex.py
@@ -0,0 +1,89 @@
+#!/usr/bin/python
+
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+from __future__ import print_function
+
+
+import subprocess
+import sys
+
+import gentoolkit.pprinter as pp
+
+import portage
+from portage import os
+
+
+class PkgIndex(object):
+ """Handle the cleaning of the binpkg Package
+ Index file
+
+ @type output: class
+ @param output: optional output class for printing
+ """
+
+ def __init__(self, controller=None):
+ self.controller = controller
+
+
+ def _get_emaint_binhost(self):
+ """Obtain a reference to the binhost module class
+
+ @sets: self.binhost to BinhostHandler class
+ @rtype: boolean
+ """
+ try:
+ self.emaint_control = Modules()
+ self.binhost = self.emaint_control._get_class('binhost')
+ except InvalidModuleName as er:
+ print( pp.error("Error importing emaint binhost module"), file=sys.stderr)
+ print( pp.error("Original error: " + er), file=sys.stderr)
+ except:
+ return False
+ return True
+
+
+ def _load_modules(self):
+ """Import the emaint modules and report the success/fail of them
+ """
+ try:
+ from emaint.module import Modules
+ from emaint.main import TaskHandler
+ except ImportError as e:
+ return False
+ return True
+
+
+ def clean_pkgs_index(self,):
+ """This will clean the binpkgs packages index file"""
+ go = self._load_modules()
+ if go:
+ if self.get_emaint_binhost():
+ self.taskmaster = TaskHandler(show_progress_bar=True)
+ tasks = [self.binhost]
+ self.taskmaster.run_tasks(tasks)
+
+
+ def call_emaint(self):
+ """Run the stand alone emaint script from
+ a subprocess call.
+
+ @rtype: integer
+ @return: the difference in file size
+ """
+ file_ = os.path.join(portage.settings['PKGDIR'], 'Packages')
+ statinfo = os.stat(file_)
+ size1 = statinfo.st_size
+ command = "emaint --fix binhost"
+ try:
+ retcode = subprocess.call(command, shell=True)
+ if retcode < 0:
+ print( pp.error("Child was terminated by signal" + str(-retcode)), file=sys.stderr)
+ except OSError as e:
+ print( pp.error("Execution failed:" + e), file=sys.stderr)
+ print()
+ statinfo = os.stat(file_)
+ clean_size = size1 - statinfo.st_size
+ self.controller(clean_size, "Packages Index", file_, "Index")
+ return clean_size
diff --git a/pym/gentoolkit/eclean/search.py b/pym/gentoolkit/eclean/search.py
new file mode 100644
index 0000000..4c5b0ac
--- /dev/null
+++ b/pym/gentoolkit/eclean/search.py
@@ -0,0 +1,520 @@
+#!/usr/bin/python
+
+# Copyright 2003-2010 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+
+from __future__ import print_function
+
+
+import re
+import stat
+import sys
+
+import portage
+from portage import os
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit.eclean.exclude import (exclDictMatchCP, exclDictExpand,
+ exclDictExpandPkgname, exclMatchFilename)
+#from gentoolkit.package import Package
+from gentoolkit.helpers import walk
+
+
+# Misc. shortcuts to some portage stuff:
+port_settings = portage.settings
+pkgdir = port_settings["PKGDIR"]
+
+err = sys.stderr
+deprecated_message=""""Deprecation Warning: Installed package: %s
+ Is no longer in the tree or an installed overlay"""
+DEPRECATED = pp.warn(deprecated_message)
+
+debug_modules = []
+
+
+def dprint(module, message):
+ if module in debug_modules:
+ print(message)
+
+
+def get_distdir():
+ """Returns DISTDIR if sane, else barfs."""
+
+ d = portage.settings["DISTDIR"]
+ if not os.path.isdir(d):
+ e = pp.error("%s does not appear to be a directory.\n" % d)
+ e += pp.error("Please set DISTDIR to a sane value.\n")
+ e += pp.error("(Check your /etc/make.conf and environment).")
+ print( e, file=sys.stderr)
+ exit(1)
+ return d
+
+distdir = get_distdir()
+
+
+class DistfilesSearch(object):
+ """
+
+ @param output: verbose output method or (lambda x: None) to turn off
+ @param vardb: defaults to portage.db[portage.root]["vartree"].dbapi
+ is overridden for testing.
+ @param portdb: defaults to portage.portdb and is overriden for testing.
+"""
+
+ def __init__(self,
+ output,
+ portdb=portage.portdb,
+ vardb=portage.db[portage.root]["vartree"].dbapi,
+ ):
+ self.vardb =vardb
+ self.portdb = portdb
+ self.output = output
+
+ def findDistfiles(self,
+ exclude={},
+ destructive=False,
+ fetch_restricted=False,
+ package_names=False,
+ time_limit=0,
+ size_limit=0,
+ _distdir=distdir,
+ deprecate=False
+ ):
+ """Find all obsolete distfiles.
+
+ XXX: what about cvs ebuilds?
+ I should install some to see where it goes...
+
+ @param exclude: an exclusion dict as defined in
+ exclude.parseExcludeFile class.
+ @param destructive: boolean, defaults to False
+ @param fetch_restricted: boolean, defaults to False
+ @param package_names: boolean, defaults to False.
+ @param time_limit: integer time value as returned by parseTime()
+ @param size_limit: integer value of max. file size to keep or 0 to ignore.
+ @param _distdir: path to the distfiles dir being checked, defaults to portage.
+ @param deprecate: bool to control checking the clean dict. files for exclusion
+
+ @rtype: dict
+ @return dict. of package files to clean i.e. {'cat/pkg-ver.tbz2': [filename],}
+ """
+ clean_me = {}
+ pkgs = {}
+ saved = {}
+ deprecated = {}
+ installed_included = False
+ # create a big CPV->SRC_URI dict of packages
+ # whose distfiles should be kept
+ if (not destructive) or fetch_restricted:
+ self.output("...non-destructive type search")
+ # TODO fix fetch_restricted to save the installed packges filenames while processing
+ pkgs, _deprecated = self._non_destructive(destructive, fetch_restricted, exclude=exclude)
+ deprecated.update(_deprecated)
+ installed_included = True
+ if destructive:
+ self.output("...destructive type search: %d packages already found" %len(pkgs))
+ pkgs, _deprecated = self._destructive(package_names,
+ exclude, pkgs, installed_included)
+ deprecated.update(_deprecated)
+ # gather the files to be cleaned
+ self.output("...checking limits for %d ebuild sources"
+ %len(pkgs))
+ clean_me = self._check_limits(_distdir,
+ size_limit, time_limit, exclude)
+ # remove any protected files from the list
+ self.output("...removing protected sources from %s candidates to clean"
+ %len(clean_me))
+ clean_me = self._remove_protected(pkgs, clean_me)
+ if not deprecate and len(exclude) and len(clean_me):
+ self.output("...checking final for exclusion from " +\
+ "%s remaining candidates to clean" %len(clean_me))
+ clean_me, saved = self._check_excludes(exclude, clean_me)
+ return clean_me, saved, deprecated
+
+
+####################### begin _check_limits code block
+
+ def _check_limits(self,
+ _distdir,
+ size_limit,
+ time_limit,
+ exclude,
+ clean_me={}
+ ):
+ """Checks files if they exceed size and/or time_limits, etc.
+ """
+ checks = [self._isreg_limit_]
+ if size_limit:
+ checks.append(self._size_limit_)
+ self.size_limit = size_limit
+ else:
+ self.output(" - skipping size limit check")
+ if time_limit:
+ checks.append(self._time_limit_)
+ self.time_limit = time_limit
+ else:
+ self.output(" - skipping time limit check")
+ if 'filenames' in exclude:
+ checks.append(self._filenames_limit_)
+ self.exclude = exclude
+ else:
+ self.output(" - skipping exclude filenames check")
+ max_index = len(checks)
+ for file in os.listdir(_distdir):
+ filepath = os.path.join(_distdir, file)
+ try:
+ file_stat = os.stat(filepath)
+ except:
+ continue
+ _index = 0
+ next = True
+ skip_file = False
+ while _index<max_index and next:
+ next, skip_file = checks[_index](file_stat, file)
+ _index +=1
+ if skip_file:
+ continue
+ # this is a candidate for cleaning
+ #print( "Adding file to clean_list:", file)
+ clean_me[file]=[filepath]
+ return clean_me
+
+ def _isreg_limit_(self, file_stat, file):
+ """check if file is a regular file."""
+ is_reg_file = stat.S_ISREG(file_stat[stat.ST_MODE])
+ return is_reg_file, not is_reg_file
+
+ def _size_limit_(self, file_stat, file):
+ """checks if the file size exceeds the size_limit"""
+ if (file_stat[stat.ST_SIZE] >= self.size_limit):
+ #print( "size match ", file, file_stat[stat.ST_SIZE])
+ return False, True
+ return True, False
+
+ def _time_limit_(self, file_stat, file):
+ """checks if the file exceeds the time_limit"""
+ if (file_stat[stat.ST_MTIME] >= self.time_limit):
+ #print( "time match ", file, file_stat[stat.ST_MTIME])
+ return False, True
+ return True,False
+
+ def _filenames_limit_(self, file_stat, file):
+ """checks if the file matches an exclusion file listing"""
+ # Try to match file name directly
+ if file in self.exclude['filenames']:
+ return False, True
+ # See if file matches via regular expression matching
+ else:
+ file_match = False
+ for file_entry in self.exclude['filenames']:
+ if self.exclude['filenames'][file_entry].match(file):
+ file_match = True
+ break
+ if file_match:
+ return False, True
+ return True, False
+
+####################### end _check_limits code block
+
+ def _remove_protected(self,
+ pkgs,
+ clean_me
+ ):
+ """Remove files owned by some protected packages.
+
+ @returns packages to clean
+ @rtype: dictionary
+ """
+ # 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(r'([a-zA-Z0-9_,\.\-\+\~]*)[\s\)]')
+ for cpv in pkgs:
+ for file in file_regexp.findall(pkgs[cpv]+"\n"):
+ if file in clean_me:
+ del clean_me[file]
+ # no need to waste IO time if there is nothing left to clean
+ if not len(clean_me):
+ return clean_me
+ return clean_me
+
+ def _non_destructive(self,
+ destructive,
+ fetch_restricted,
+ pkgs_ = {},
+ exclude={}
+ ):
+ """performs the non-destructive checks
+
+ @param destructive: boolean
+ @param pkgs_: starting dictionary to add to
+ defaults to {}.
+
+ @returns packages and thier SRC_URI's: {cpv: src_uri,}
+ @rtype: dictionary
+ """
+ pkgs = pkgs_.copy()
+ deprecated = {}
+ # the following code block was split to optimize for speed
+ # list all CPV from portree (yeah, that takes time...)
+ self.output(" - getting complete ebuild list")
+ cpvs = set(self.portdb.cpv_all())
+ # now add any installed cpv's that are not in the tree or overlays
+ installed_cpvs = self.vardb.cpv_all()
+ cpvs.update(installed_cpvs)
+ if fetch_restricted and destructive:
+ self.output(" - getting source file names " +
+ "for %d installed ebuilds" %len(installed_cpvs))
+ pkgs, _deprecated = self._unrestricted(pkgs, installed_cpvs)
+ deprecated.update(_deprecated)
+ # remove the installed cpvs then check the remaining for fetch restiction
+ cpvs.difference_update(installed_cpvs)
+ self.output(" - getting fetch-restricted source file names " +
+ "for %d remaining ebuilds" %len(cpvs))
+ pkgs, _deprecated = self._fetch_restricted(destructive, pkgs, cpvs)
+ deprecated.update(_deprecated)
+ else:
+ self.output(" - getting source file names " +
+ "for %d ebuilds" %len(cpvs))
+ pkgs, _deprecated = self._unrestricted(pkgs, cpvs)
+ deprecated.update(_deprecated)
+ return pkgs, deprecated
+
+ def _fetch_restricted(self, destructive, pkgs_, cpvs):
+ """perform fetch restricted non-destructive source
+ filename lookups
+
+ @param destructive: boolean
+ @param pkgs_: starting dictionary to add to
+ @param cpvs: set of (cat/pkg-ver, ...) identifiers
+
+ @return a new pkg dictionary
+ @rtype: dictionary
+ """
+ pkgs = pkgs_.copy()
+ deprecated = {}
+ for cpv in cpvs:
+ # get SRC_URI and RESTRICT from aux_get
+ try: # main portdb
+ (src_uri,restrict) = \
+ self.portdb.aux_get(cpv,["SRC_URI","RESTRICT"])
+ # keep fetch-restricted check
+ # inside try so it is bypassed on KeyError
+ if 'fetch' in restrict:
+ pkgs[cpv] = src_uri
+ except KeyError:
+ try: # installed vardb
+ (src_uri,restrict) = \
+ self.vardb.aux_get(cpv,["SRC_URI","RESTRICT"])
+ deprecated[cpv] = src_uri
+ self.output(DEPRECATED %cpv)
+ # keep fetch-restricted check
+ # inside try so it is bypassed on KeyError
+ if 'fetch' in restrict:
+ pkgs[cpv] = src_uri
+ except KeyError:
+ self.output(" - Key Error looking up: " + cpv)
+ return pkgs, deprecated
+
+ def _unrestricted(self, pkgs_, cpvs):
+ """Perform unrestricted source filenames lookups
+
+ @param pkgs_: starting packages dictionary
+ @param cpvs: set of (cat/pkg-ver, ...) identifiers
+
+ @return a new pkg dictionary
+ @rtype: dictionary
+ """
+ pkgs = pkgs_.copy()
+ deprecated = {}
+ for cpv in cpvs:
+ # get SRC_URI from aux_get
+ try:
+ pkgs[cpv] = self.portdb.aux_get(cpv,["SRC_URI"])[0]
+ except KeyError:
+ try: # installed vardb
+ pkgs[cpv] = self.vardb.aux_get(cpv,["SRC_URI"])[0]
+ deprecated[cpv] = pkgs[cpv]
+ self.output(DEPRECATED %cpv)
+ except KeyError:
+ self.output(" - Key Error looking up: " + cpv)
+ return pkgs, deprecated
+
+ def _destructive(self,
+ package_names,
+ exclude,
+ pkgs_={},
+ installed_included=False
+ ):
+ """Builds on pkgs according to input options
+
+ @param package_names: boolean
+ @param exclude: an exclusion dict as defined in
+ exclude.parseExcludeFile class.
+ @param pkgs: starting dictionary to add to
+ defaults to {}.
+ @param installed_included: bool. pkgs already
+ has the installed cpv's added.
+
+ @returns pkgs: {cpv: src_uri,}
+ """
+ pkgs = pkgs_.copy()
+ deprecated = {}
+ pkgset = set()
+ if not installed_included:
+ if not package_names:
+ # list all installed CPV's from vartree
+ #print( "_destructive: getting vardb.cpv_all")
+ pkgset.update(self.vardb.cpv_all())
+ self.output(" - processing %s installed ebuilds" % len(pkgset))
+ elif package_names:
+ # list all CPV's from portree for CP's in vartree
+ #print( "_destructive: getting vardb.cp_all")
+ cps = self.vardb.cp_all()
+ self.output(" - processing %s installed packages" % len(cps))
+ for package in cps:
+ pkgset.update(self.portdb.cp_list(package))
+ self.output(" - processing excluded")
+ excludes = self._get_excludes(exclude)
+ excludes_length = len(excludes)
+ pkgset.update(excludes)
+ pkgs_done = set(list(pkgs))
+ pkgset.difference_update(pkgs_done)
+ self.output(
+ " - (%d of %d total) additional excluded packages to get source filenames for"
+ %(len(pkgset), excludes_length))
+ #self.output(" - processing %d ebuilds for filenames" %len(pkgset))
+ pkgs, _deprecated = self._unrestricted(pkgs, pkgset)
+ deprecated.update(_deprecated)
+ #self.output(" - done...")
+ return pkgs, deprecated
+
+ def _get_excludes(self, exclude):
+ """Expands the exclude dictionary into a set of
+ CPV's
+
+ @param exclude: dictionary of exclusion categories,
+ packages to exclude from the cleaning
+
+ @rtype: set
+ @return set of package cpv's
+ """
+ pkgset = set()
+ for cp in exclDictExpand(exclude):
+ # add packages from the exclude file
+ pkgset.update(self.portdb.cp_list(cp))
+ return pkgset
+
+ def _check_excludes(self, exclude, clean_me):
+ """Performs a last minute check on remaining filenames
+ to see if they should be protected. Since if the pkg-version
+ was deprecated it would not have been matched to a
+ source filename and removed.
+
+ @param exclude: an exclusion dictionary
+ @param clean_me: the list of filenames for cleaning
+
+ @rtype: dict of packages to clean
+ """
+ saved = {}
+ pn_excludes = exclDictExpandPkgname(exclude)
+ dprint("excludes", "_check_excludes: made it here ;)")
+ if not pn_excludes:
+ return clean_me, saved
+ dprint("excludes", pn_excludes)
+ for key in list(clean_me):
+ if exclMatchFilename(pn_excludes, key):
+ saved[key] = clean_me[key]
+ del clean_me[key]
+ self.output(" ...Saved excluded package filename: " + key)
+ return clean_me, saved
+
+
+def findPackages(
+ options,
+ exclude={},
+ destructive=False,
+ time_limit=0,
+ package_names=False,
+ pkgdir=None,
+ port_dbapi=portage.db[portage.root]["porttree"].dbapi,
+ var_dbapi=portage.db[portage.root]["vartree"].dbapi
+ ):
+ """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.
+
+ @param options: dict of options determined at runtime
+ @param exclude: an exclusion dict as defined in
+ exclude.parseExcludeFile class.
+ @param destructive: boolean, defaults to False
+ @param time_limit: integer time value as returned by parseTime()
+ @param package_names: boolean, defaults to False.
+ used only if destructive=True
+ @param pkgdir: path to the binary package dir being checked
+ @param port_dbapi: defaults to portage.db[portage.root]["porttree"].dbapi
+ can be overridden for tests.
+ @param var_dbapi: defaults to portage.db[portage.root]["vartree"].dbapi
+ can be overridden for tests.
+
+ @rtype: dict
+ @return clean_me i.e. {'cat/pkg-ver.tbz2': [filepath],}
+ """
+ clean_me = {}
+ # create a full package dictionary
+
+ # now do an access test, os.walk does not error for "no read permission"
+ try:
+ test = os.listdir(pkgdir)
+ del test
+ except EnvironmentError as er:
+ print( pp.error("Error accessing PKGDIR." ), file=sys.stderr)
+ print( pp.error("(Check your /etc/make.conf and environment)."), file=sys.stderr)
+ print( pp.error("Error: %s" %str(er)), file=sys.stderr)
+ exit(1)
+ for root, dirs, files in 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]
+ st = os.lstat(path)
+ if time_limit and (st[stat.ST_MTIME] >= time_limit):
+ # time-limit exclusion
+ continue
+ # dict is cpv->[files] (2 files in general, because of symlink)
+ clean_me[cpv] = [path]
+ #if os.path.islink(path):
+ if stat.S_ISLNK(st[stat.ST_MODE]):
+ clean_me[cpv].append(os.path.realpath(path))
+ # keep only obsolete ones
+ if destructive:
+ dbapi = var_dbapi
+ if package_names:
+ cp_all = dict.fromkeys(dbapi.cp_all())
+ else:
+ cp_all = {}
+ else:
+ dbapi = port_dbapi
+ cp_all = {}
+ for cpv in list(clean_me):
+ if exclDictMatchCP(exclude,portage.cpv_getkey(cpv)):
+ # exclusion because of the exclude file
+ del clean_me[cpv]
+ continue
+ if dbapi.cpv_exists(cpv):
+ # exclusion because pkg still exists (in porttree or vartree)
+ del clean_me[cpv]
+ continue
+ if portage.cpv_getkey(cpv) in cp_all:
+ # exlusion because of --package-names
+ del clean_me[cpv]
+
+ return clean_me