3 # Copyright 2003-2010 Gentoo Foundation
4 # Distributed under the terms of the GNU General Public License v2
7 from __future__ import print_function
10 __author__ = "Thomas de Grenier de Latour (tgl), " + \
11 "modular re-write by: Brian Dolbec (dol-sen)"
12 __email__ = "degrenier@easyconnect.fr, " + \
13 "brian.dolbec@gmail.com"
15 __productname__ = "eclean"
16 __description__ = "A cleaning tool for Gentoo distfiles and binaries."
26 from portage.output import white, yellow, turquoise, green, teal, red
28 import gentoolkit.pprinter as pp
29 from gentoolkit.eclean.search import (DistfilesSearch,
30 findPackages, port_settings, pkgdir)
31 from gentoolkit.eclean.exclude import (parseExcludeFile,
32 ParseExcludeFileException)
33 from gentoolkit.eclean.clean import CleanUp
34 from gentoolkit.eclean.output import OutputControl
35 #from gentoolkit.eclean.dbapi import Dbapi
36 from gentoolkit.eprefix import EPREFIX
39 """Output the version info."""
40 print( "%s (%s) - %s" \
41 % (__productname__, __version__, __description__))
43 print("Author: %s <%s>" % (__author__,__email__))
44 print("Copyright 2003-2009 Gentoo Foundation")
45 print("Distributed under the terms of the GNU General Public License v2")
48 def printUsage(_error=None, help=None):
49 """Print help message. May also print partial help to stderr if an
50 error from {'options','actions'} is specified."""
55 if not _error in ('actions', 'global-options', \
56 'packages-options', 'distfiles-options', \
57 'merged-packages-options', 'merged-distfiles-options', \
60 if not _error and not help: help = 'all'
62 print( pp.error("Wrong time specification"), file=out)
63 print( "Time specification should be an integer followed by a"+
64 " single letter unit.", file=out)
65 print( "Available units are: y (years), m (months), w (weeks), "+
66 "d (days) and h (hours).", file=out)
67 print( "For instance: \"1y\" is \"one year\", \"2w\" is \"two"+
68 " weeks\", etc. ", file=out)
71 print( pp.error("Wrong size specification"), file=out)
72 print( "Size specification should be an integer followed by a"+
73 " single letter unit.", file=out)
74 print( "Available units are: G, M, K and B.", file=out)
75 print("For instance: \"10M\" is \"ten megabytes\", \"200K\" "+
76 "is \"two hundreds kilobytes\", etc.", file=out)
78 if _error in ('global-options', 'packages-options', 'distfiles-options', \
79 'merged-packages-options', 'merged-distfiles-options',):
80 print( pp.error("Wrong option on command line."), file=out)
82 elif _error == 'actions':
83 print( pp.error("Wrong or missing action name on command line."), file=out)
85 print( white("Usage:"), file=out)
86 if _error in ('actions','global-options', 'packages-options', \
87 'distfiles-options') or help == 'all':
88 print( " "+turquoise(__productname__),
89 yellow("[global-option] ..."),
91 yellow("[action-option] ..."), file=out)
92 if _error == 'merged-distfiles-options' or help in ('all','distfiles'):
93 print( " "+turquoise(__productname__+'-dist'),
94 yellow("[global-option, distfiles-option] ..."), file=out)
95 if _error == 'merged-packages-options' or help in ('all','packages'):
96 print( " "+turquoise(__productname__+'-pkg'),
97 yellow("[global-option, packages-option] ..."), file=out)
98 if _error in ('global-options', 'actions'):
99 print( " "+turquoise(__productname__),
100 yellow("[--help, --version]"), file=out)
102 print( " "+turquoise(__productname__+"(-dist,-pkg)"),
103 yellow("[--help, --version]"), file=out)
104 if _error == 'merged-packages-options' or help == 'packages':
105 print( " "+turquoise(__productname__+'-pkg'),
106 yellow("[--help, --version]"), file=out)
107 if _error == 'merged-distfiles-options' or help == 'distfiles':
108 print( " "+turquoise(__productname__+'-dist'),
109 yellow("[--help, --version]"), file=out)
111 if _error in ('global-options', 'merged-packages-options', \
112 'merged-distfiles-options') or help:
113 print( "Available global", yellow("options")+":", file=out)
114 print( yellow(" -C, --nocolor")+
115 " - turn off colors on output", file=out)
116 print( yellow(" -d, --destructive")+
117 " - only keep the minimum for a reinstallation", file=out)
118 print( yellow(" -e, --exclude-file=<path>")+
119 " - path to the exclusion file", file=out)
120 print( yellow(" -i, --interactive")+
121 " - ask confirmation before deletions", file=out)
122 print( yellow(" -n, --package-names")+
123 " - protect all versions (when --destructive)", file=out)
124 print( yellow(" -p, --pretend")+
125 " - only display what would be cleaned", file=out)
126 print( yellow(" -q, --quiet")+
127 " - be as quiet as possible", file=out)
128 print( yellow(" -t, --time-limit=<time>")+
129 " - don't delete files modified since "+yellow("<time>"), file=out)
130 print( " "+yellow("<time>"), "is a duration: \"1y\" is"+
131 " \"one year\", \"2w\" is \"two weeks\", etc. ", file=out)
132 print( " "+"Units are: y (years), m (months), w (weeks), "+
133 "d (days) and h (hours).", file=out)
134 print( yellow(" -h, --help")+ \
135 " - display the help screen", file=out)
136 print( yellow(" -V, --version")+
137 " - display version info", file=out)
139 if _error == 'actions' or help == 'all':
140 print( "Available", green("actions")+":", file=out)
141 print( green(" packages")+
142 " - clean outdated binary packages from PKGDIR", file=out)
143 print( green(" distfiles")+
144 " - clean outdated packages sources files from DISTDIR", file=out)
146 if _error in ('packages-options','merged-packages-options') \
147 or help in ('all','packages'):
148 print( "Available", yellow("options"),"for the",
149 green("packages"),"action:", file=out)
150 print( yellow(" NONE :)"), file=out)
152 if _error in ('distfiles-options', 'merged-distfiles-options') \
153 or help in ('all','distfiles'):
154 print("Available", yellow("options"),"for the",
155 green("distfiles"),"action:", file=out)
156 print( yellow(" -f, --fetch-restricted")+
157 " - protect fetch-restricted files (when --destructive)", file=out)
158 print( yellow(" -s, --size-limit=<size>")+
159 " - don't delete distfiles bigger than "+yellow("<size>"), file=out)
160 print( " "+yellow("<size>"), "is a size specification: "+
161 "\"10M\" is \"ten megabytes\", \"200K\" is", file=out)
162 print( " "+"\"two hundreds kilobytes\", etc. Units are: "+
163 "G, M, K and B.", file=out)
165 print( "More detailed instruction can be found in",
166 turquoise("`man %s`" % __productname__), file=out)
169 class ParseArgsException(Exception):
170 """For parseArgs() -> main() communications."""
171 def __init__(self, value):
172 self.value = value # sdfgsdfsdfsd
174 return repr(self.value)
178 """Convert a file size "Xu" ("X" is an integer, and "u" in
179 [G,M,K,B]) into an integer (file size in Bytes).
181 @raise ParseArgsException: in case of failure
190 match = re.match(r"^(?P<value>\d+)(?P<unit>[GMKBgmkb])?$",size)
191 size = int(match.group('value'))
192 if match.group('unit'):
193 size *= units[match.group('unit').capitalize()]
195 raise ParseArgsException('size')
199 def parseTime(timespec):
200 """Convert a duration "Xu" ("X" is an int, and "u" a time unit in
201 [Y,M,W,D,H]) into an integer which is a past EPOCH date.
202 Raises ParseArgsException('time') in case of failure.
203 (yep, big approximations inside... who cares?).
205 units = {'H' : (60 * 60)}
206 units['D'] = units['H'] * 24
207 units['W'] = units['D'] * 7
208 units['M'] = units['D'] * 30
209 units['Y'] = units['D'] * 365
211 # parse the time specification
212 match = re.match(r"^(?P<value>\d+)(?P<unit>[YMWDHymwdh])?$",timespec)
213 value = int(match.group('value'))
214 if not match.group('unit'): unit = 'D'
215 else: unit = match.group('unit').capitalize()
217 raise ParseArgsException('time')
218 return time.time() - (value * units[unit])
221 def parseArgs(options={}):
222 """Parse the command line arguments. Raise exceptions on
223 errors or non-action modes (help/version). Returns an action, and affect
227 def optionSwitch(option,opts,action=None):
228 """local function for interpreting command line options
229 and setting options accordingly"""
232 if o in ("-h", "--help"):
234 raise ParseArgsException('help-'+action)
236 raise ParseArgsException('help')
237 elif o in ("-V", "--version"):
238 raise ParseArgsException('version')
239 elif o in ("-C", "--nocolor"):
240 options['nocolor'] = True
242 elif o in ("-d", "--destructive"):
243 options['destructive'] = True
244 elif o in ("-D", "--deprecated"):
245 options['deprecated'] = True
246 elif o in ("-i", "--interactive") and not options['pretend']:
247 options['interactive'] = True
248 elif o in ("-p", "--pretend"):
249 options['pretend'] = True
250 options['interactive'] = False
251 elif o in ("-q", "--quiet"):
252 options['quiet'] = True
253 options['verbose'] = False
254 elif o in ("-t", "--time-limit"):
255 options['time-limit'] = parseTime(a)
256 elif o in ("-e", "--exclude-file"):
257 print("cli --exclude option")
258 options['exclude-file'] = a
259 elif o in ("-n", "--package-names"):
260 options['package-names'] = True
261 elif o in ("-f", "--fetch-restricted"):
262 options['fetch-restricted'] = True
263 elif o in ("-s", "--size-limit"):
264 options['size-limit'] = parseSize(a)
265 elif o in ("-v", "--verbose") and not options['quiet']:
266 options['verbose'] = True
269 # sanity check of --destructive only options:
270 for opt in ('fetch-restricted', 'package-names'):
271 if (not options['destructive']) and options[opt]:
272 if not options['quiet']:
274 "--%s only makes sense in --destructive mode." % opt), file=sys.stderr)
278 # here are the different allowed command line options (getopt args)
279 getopt_options = {'short':{}, 'long':{}}
280 getopt_options['short']['global'] = "CdDipqe:t:nhVv"
281 getopt_options['long']['global'] = ["nocolor", "destructive",
282 "deprecated", "interactive", "pretend", "quiet", "exclude-file=",
283 "time-limit=", "package-names", "help", "version", "verbose"]
284 getopt_options['short']['distfiles'] = "fs:"
285 getopt_options['long']['distfiles'] = ["fetch-restricted", "size-limit="]
286 getopt_options['short']['packages'] = ""
287 getopt_options['long']['packages'] = [""]
288 # set default options, except 'nocolor', which is set in main()
289 options['interactive'] = False
290 options['pretend'] = False
291 options['quiet'] = False
292 options['accept_all'] = False
293 options['destructive'] = False
294 options['deprecated'] = False
295 options['time-limit'] = 0
296 options['package-names'] = False
297 options['fetch-restricted'] = False
298 options['size-limit'] = 0
299 options['verbose'] = False
300 # if called by a well-named symlink, set the acction accordingly:
302 # temp print line to ensure it is the svn/branch code running, etc..
303 #print( "###### svn/branch/gentoolkit_eclean ####### ==> ", os.path.basename(sys.argv[0]))
304 if os.path.basename(sys.argv[0]) in \
305 (__productname__+'-pkg', __productname__+'-packages'):
307 elif os.path.basename(sys.argv[0]) in \
308 (__productname__+'-dist', __productname__+'-distfiles'):
310 # prepare for the first getopt
312 short_opts = getopt_options['short']['global'] \
313 + getopt_options['short'][action]
314 long_opts = getopt_options['long']['global'] \
315 + getopt_options['long'][action]
316 opts_mode = 'merged-'+action
318 short_opts = getopt_options['short']['global']
319 long_opts = getopt_options['long']['global']
321 # apply getopts to command line, show partial help on failure
323 opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
325 raise ParseArgsException(opts_mode+'-options')
326 # set options accordingly
327 optionSwitch(options,opts,action=action)
328 # if action was already set, there should be no more args
329 if action and len(args):
330 raise ParseArgsException(opts_mode+'-options')
331 # if action was set, there is nothing left to do
334 # So, we are in "eclean --foo action --bar" mode. Parse remaining args...
335 # Only two actions are allowed: 'packages' and 'distfiles'.
336 if not len(args) or not args[0] in ('packages','distfiles'):
337 raise ParseArgsException('actions')
339 # parse the action specific options
341 opts, args = getopt.getopt(args, \
342 getopt_options['short'][action], \
343 getopt_options['long'][action])
345 raise ParseArgsException(action+'-options')
346 # set options again, for action-specific options
347 optionSwitch(options,opts,action=action)
348 # any remaning args? Then die!
350 raise ParseArgsException(action+'-options')
351 # returns the action. Options dictionary is modified by side-effect.
355 def doAction(action,options,exclude={}, output=None):
356 """doAction: execute one action, ie display a few message, call the right
357 find* function, and then call doCleanup with its result."""
358 # define vocabulary for the output
359 if action == 'packages':
360 files_type = "binary packages"
362 files_type = "distfiles"
365 # find files to delete, depending on the action
366 if not options['quiet']:
367 output.einfo("Building file list for "+action+" cleaning...")
368 if action == 'packages':
369 clean_me = findPackages(
372 destructive=options['destructive'],
373 package_names=options['package-names'],
374 time_limit=options['time-limit'],
376 #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
377 #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
381 engine = DistfilesSearch(output=options['verbose-output'],
382 #portdb=Dbapi(portage.db[portage.root]["porttree"].dbapi),
383 #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
385 clean_me, saved, deprecated = engine.findDistfiles(
387 destructive=options['destructive'],
388 fetch_restricted=options['fetch-restricted'],
389 package_names=options['package-names'],
390 time_limit=options['time-limit'],
391 size_limit=options['size-limit'],
392 deprecate = options['deprecated']
394 # actually clean files if something was found
396 # verbose pretend message
397 if options['pretend'] and not options['quiet']:
398 output.einfo("Here are the "+files_type+" that would be deleted:")
399 # verbose non-pretend message
400 elif not options['quiet']:
401 output.einfo("Cleaning " + files_type +"...")
402 # do the cleanup, and get size of deleted files
403 cleaner = CleanUp( output.progress_controller)
404 if options['pretend']:
405 clean_size = cleaner.pretend_clean(clean_me)
406 elif action in ['distfiles']:
407 clean_size = cleaner.clean_dist(clean_me)
408 elif action in ['packages']:
409 clean_size = cleaner.clean_pkgs(clean_me,
411 # vocabulary for final message
412 if options['pretend']:
416 # display freed space
417 if not options['quiet']:
418 output.total('normal', clean_size, len(clean_me), verb, action)
419 # nothing was found, return
420 elif not options['quiet']:
421 output.einfo("Your "+action+" directory was already clean.")
422 if saved and not options['quiet']:
424 print( (pp.emph(" The following ") + yellow("unavailable") +
425 pp.emph(" files were saved from cleaning due to exclusion file entries")))
426 output.set_colors('deprecated')
427 clean_size = cleaner.pretend_clean(saved)
428 output.total('deprecated', clean_size, len(saved), verb, action)
429 if deprecated and not options['quiet']:
431 print( (pp.emph(" The following ") + yellow("unavailable") +
432 pp.emph(" installed packages were found")))
433 output.set_colors('deprecated')
434 output.list_pkgs(deprecated)
438 """Parse command line and execute all actions."""
439 # set default options
441 options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
442 or not sys.stdout.isatty())
443 if options['nocolor']:
445 # parse command line options and actions
447 action = parseArgs(options)
448 # filter exception to know what message to display
449 except ParseArgsException as e:
450 if e.value == 'help':
451 printUsage(help='all')
453 elif e.value[:5] == 'help-':
454 printUsage(help=e.value[5:])
456 elif e.value == 'version':
462 output = OutputControl(options)
463 options['verbose-output'] = lambda x: None
464 if not options['quiet']:
465 if options['verbose']:
466 options['verbose-output'] = output.einfo
467 # parse the exclusion file
468 if not 'exclude-file' in options:
469 # set it to the default exclude file if it exists
470 exclude_file = "%s/etc/%s/%s.exclude" % (EPREFIX,__productname__ , action)
471 if os.path.isfile(exclude_file):
472 options['exclude-file'] = exclude_file
473 if 'exclude-file' in options:
475 exclude = parseExcludeFile(options['exclude-file'],
476 options['verbose-output'])
477 except ParseExcludeFileException as e:
478 print( pp.error(str(e)), file=sys.stderr)
480 "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
482 "See format of this file in `man %s`" % __productname__), file=sys.stderr)
486 # security check for non-pretend mode
487 if not options['pretend'] and portage.secpass == 0:
489 "Permission denied: you must be root or belong to " +
490 "the portage group."), file=sys.stderr)
493 doAction(action, options, exclude=exclude,
497 if __name__ == "__main__":
498 """actually call main() if launched as a script"""
501 except KeyboardInterrupt: