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."
25 from portage import os
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
38 """Output the version info."""
39 print( "%s (%s) - %s" \
40 % (__productname__, __version__, __description__))
42 print("Author: %s <%s>" % (__author__,__email__))
43 print("Copyright 2003-2009 Gentoo Foundation")
44 print("Distributed under the terms of the GNU General Public License v2")
47 def printUsage(_error=None, help=None):
48 """Print help message. May also print partial help to stderr if an
49 error from {'options','actions'} is specified."""
54 if not _error in ('actions', 'global-options', \
55 'packages-options', 'distfiles-options', \
56 'merged-packages-options', 'merged-distfiles-options', \
59 if not _error and not help: help = 'all'
61 print( pp.error("Wrong time specification"), file=out)
62 print( "Time specification should be an integer followed by a"+
63 " single letter unit.", file=out)
64 print( "Available units are: y (years), m (months), w (weeks), "+
65 "d (days) and h (hours).", file=out)
66 print( "For instance: \"1y\" is \"one year\", \"2w\" is \"two"+
67 " weeks\", etc. ", file=out)
70 print( pp.error("Wrong size specification"), file=out)
71 print( "Size specification should be an integer followed by a"+
72 " single letter unit.", file=out)
73 print( "Available units are: G, M, K and B.", file=out)
74 print("For instance: \"10M\" is \"ten megabytes\", \"200K\" "+
75 "is \"two hundreds kilobytes\", etc.", file=out)
77 if _error in ('global-options', 'packages-options', 'distfiles-options', \
78 'merged-packages-options', 'merged-distfiles-options',):
79 print( pp.error("Wrong option on command line."), file=out)
81 elif _error == 'actions':
82 print( pp.error("Wrong or missing action name on command line."), file=out)
84 print( white("Usage:"), file=out)
85 if _error in ('actions','global-options', 'packages-options', \
86 'distfiles-options') or help == 'all':
87 print( " "+turquoise(__productname__),
88 yellow("[global-option] ..."),
90 yellow("[action-option] ..."), file=out)
91 if _error == 'merged-distfiles-options' or help in ('all','distfiles'):
92 print( " "+turquoise(__productname__+'-dist'),
93 yellow("[global-option, distfiles-option] ..."), file=out)
94 if _error == 'merged-packages-options' or help in ('all','packages'):
95 print( " "+turquoise(__productname__+'-pkg'),
96 yellow("[global-option, packages-option] ..."), file=out)
97 if _error in ('global-options', 'actions'):
98 print( " "+turquoise(__productname__),
99 yellow("[--help, --version]"), file=out)
101 print( " "+turquoise(__productname__+"(-dist,-pkg)"),
102 yellow("[--help, --version]"), file=out)
103 if _error == 'merged-packages-options' or help == 'packages':
104 print( " "+turquoise(__productname__+'-pkg'),
105 yellow("[--help, --version]"), file=out)
106 if _error == 'merged-distfiles-options' or help == 'distfiles':
107 print( " "+turquoise(__productname__+'-dist'),
108 yellow("[--help, --version]"), file=out)
110 if _error in ('global-options', 'merged-packages-options', \
111 'merged-distfiles-options') or help:
112 print( "Available global", yellow("options")+":", file=out)
113 print( yellow(" -C, --nocolor")+
114 " - turn off colors on output", file=out)
115 print( yellow(" -d, --destructive")+
116 " - only keep the minimum for a reinstallation", file=out)
117 print( yellow(" -e, --exclude-file=<path>")+
118 " - path to the exclusion file", file=out)
119 print( yellow(" -i, --interactive")+
120 " - ask confirmation before deletions", file=out)
121 print( yellow(" -n, --package-names")+
122 " - protect all versions (when --destructive)", file=out)
123 print( yellow(" -p, --pretend")+
124 " - only display what would be cleaned", file=out)
125 print( yellow(" -q, --quiet")+
126 " - be as quiet as possible", file=out)
127 print( yellow(" -t, --time-limit=<time>")+
128 " - don't delete files modified since "+yellow("<time>"), file=out)
129 print( " "+yellow("<time>"), "is a duration: \"1y\" is"+
130 " \"one year\", \"2w\" is \"two weeks\", etc. ", file=out)
131 print( " "+"Units are: y (years), m (months), w (weeks), "+
132 "d (days) and h (hours).", file=out)
133 print( yellow(" -h, --help")+ \
134 " - display the help screen", file=out)
135 print( yellow(" -V, --version")+
136 " - display version info", file=out)
138 if _error == 'actions' or help == 'all':
139 print( "Available", green("actions")+":", file=out)
140 print( green(" packages")+
141 " - clean outdated binary packages from PKGDIR", file=out)
142 print( green(" distfiles")+
143 " - clean outdated packages sources files from DISTDIR", file=out)
145 if _error in ('packages-options','merged-packages-options') \
146 or help in ('all','packages'):
147 print( "Available", yellow("options"),"for the",
148 green("packages"),"action:", file=out)
149 print( yellow(" NONE :)"), file=out)
151 if _error in ('distfiles-options', 'merged-distfiles-options') \
152 or help in ('all','distfiles'):
153 print("Available", yellow("options"),"for the",
154 green("distfiles"),"action:", file=out)
155 print( yellow(" -f, --fetch-restricted")+
156 " - protect fetch-restricted files (when --destructive)", file=out)
157 print( yellow(" -s, --size-limit=<size>")+
158 " - don't delete distfiles bigger than "+yellow("<size>"), file=out)
159 print( " "+yellow("<size>"), "is a size specification: "+
160 "\"10M\" is \"ten megabytes\", \"200K\" is", file=out)
161 print( " "+"\"two hundreds kilobytes\", etc. Units are: "+
162 "G, M, K and B.", file=out)
164 print( "More detailed instruction can be found in",
165 turquoise("`man %s`" % __productname__), file=out)
168 class ParseArgsException(Exception):
169 """For parseArgs() -> main() communications."""
170 def __init__(self, value):
171 self.value = value # sdfgsdfsdfsd
173 return repr(self.value)
177 """Convert a file size "Xu" ("X" is an integer, and "u" in
178 [G,M,K,B]) into an integer (file size in Bytes).
180 @raise ParseArgsException: in case of failure
189 match = re.match(r"^(?P<value>\d+)(?P<unit>[GMKBgmkb])?$",size)
190 size = int(match.group('value'))
191 if match.group('unit'):
192 size *= units[match.group('unit').capitalize()]
194 raise ParseArgsException('size')
198 def parseTime(timespec):
199 """Convert a duration "Xu" ("X" is an int, and "u" a time unit in
200 [Y,M,W,D,H]) into an integer which is a past EPOCH date.
201 Raises ParseArgsException('time') in case of failure.
202 (yep, big approximations inside... who cares?).
204 units = {'H' : (60 * 60)}
205 units['D'] = units['H'] * 24
206 units['W'] = units['D'] * 7
207 units['M'] = units['D'] * 30
208 units['Y'] = units['D'] * 365
210 # parse the time specification
211 match = re.match(r"^(?P<value>\d+)(?P<unit>[YMWDHymwdh])?$",timespec)
212 value = int(match.group('value'))
213 if not match.group('unit'): unit = 'D'
214 else: unit = match.group('unit').capitalize()
216 raise ParseArgsException('time')
217 return time.time() - (value * units[unit])
220 def parseArgs(options={}):
221 """Parse the command line arguments. Raise exceptions on
222 errors or non-action modes (help/version). Returns an action, and affect
226 def optionSwitch(option,opts,action=None):
227 """local function for interpreting command line options
228 and setting options accordingly"""
231 if o in ("-h", "--help"):
233 raise ParseArgsException('help-'+action)
235 raise ParseArgsException('help')
236 elif o in ("-V", "--version"):
237 raise ParseArgsException('version')
238 elif o in ("-C", "--nocolor"):
239 options['nocolor'] = True
241 elif o in ("-d", "--destructive"):
242 options['destructive'] = True
243 elif o in ("-D", "--deprecated"):
244 options['deprecated'] = True
245 elif o in ("-i", "--interactive") and not options['pretend']:
246 options['interactive'] = True
247 elif o in ("-p", "--pretend"):
248 options['pretend'] = True
249 options['interactive'] = False
250 elif o in ("-q", "--quiet"):
251 options['quiet'] = True
252 options['verbose'] = False
253 elif o in ("-t", "--time-limit"):
254 options['time-limit'] = parseTime(a)
255 elif o in ("-e", "--exclude-file"):
256 options['exclude-file'] = a
257 elif o in ("-n", "--package-names"):
258 options['package-names'] = True
259 elif o in ("-f", "--fetch-restricted"):
260 options['fetch-restricted'] = True
261 elif o in ("-s", "--size-limit"):
262 options['size-limit'] = parseSize(a)
263 elif o in ("-v", "--verbose") and not options['quiet']:
264 options['verbose'] = True
267 # sanity check of --destructive only options:
268 for opt in ('fetch-restricted', 'package-names'):
269 if (not options['destructive']) and options[opt]:
270 if not options['quiet']:
272 "--%s only makes sense in --destructive mode." % opt), file=sys.stderr)
276 # here are the different allowed command line options (getopt args)
277 getopt_options = {'short':{}, 'long':{}}
278 getopt_options['short']['global'] = "CdDipqe:t:nhVv"
279 getopt_options['long']['global'] = ["nocolor", "destructive",
280 "deprecated", "interactive", "pretend", "quiet", "exclude-file=",
281 "time-limit=", "package-names", "help", "version", "verbose"]
282 getopt_options['short']['distfiles'] = "fs:"
283 getopt_options['long']['distfiles'] = ["fetch-restricted", "size-limit="]
284 getopt_options['short']['packages'] = ""
285 getopt_options['long']['packages'] = [""]
286 # set default options, except 'nocolor', which is set in main()
287 options['interactive'] = False
288 options['pretend'] = False
289 options['quiet'] = False
290 options['accept_all'] = False
291 options['destructive'] = False
292 options['deprecated'] = False
293 options['time-limit'] = 0
294 options['package-names'] = False
295 options['fetch-restricted'] = False
296 options['size-limit'] = 0
297 options['verbose'] = False
298 # if called by a well-named symlink, set the acction accordingly:
300 # temp print line to ensure it is the svn/branch code running, etc..
301 #print( "###### svn/branch/gentoolkit_eclean ####### ==> ", os.path.basename(sys.argv[0]))
302 if os.path.basename(sys.argv[0]) in \
303 (__productname__+'-pkg', __productname__+'-packages'):
305 elif os.path.basename(sys.argv[0]) in \
306 (__productname__+'-dist', __productname__+'-distfiles'):
308 # prepare for the first getopt
310 short_opts = getopt_options['short']['global'] \
311 + getopt_options['short'][action]
312 long_opts = getopt_options['long']['global'] \
313 + getopt_options['long'][action]
314 opts_mode = 'merged-'+action
316 short_opts = getopt_options['short']['global']
317 long_opts = getopt_options['long']['global']
319 # apply getopts to command line, show partial help on failure
321 opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
323 raise ParseArgsException(opts_mode+'-options')
324 # set options accordingly
325 optionSwitch(options,opts,action=action)
326 # if action was already set, there should be no more args
327 if action and len(args):
328 raise ParseArgsException(opts_mode+'-options')
329 # if action was set, there is nothing left to do
332 # So, we are in "eclean --foo action --bar" mode. Parse remaining args...
333 # Only two actions are allowed: 'packages' and 'distfiles'.
334 if not len(args) or not args[0] in ('packages','distfiles'):
335 raise ParseArgsException('actions')
337 # parse the action specific options
339 opts, args = getopt.getopt(args, \
340 getopt_options['short'][action], \
341 getopt_options['long'][action])
343 raise ParseArgsException(action+'-options')
344 # set options again, for action-specific options
345 optionSwitch(options,opts,action=action)
346 # any remaning args? Then die!
348 raise ParseArgsException(action+'-options')
349 # returns the action. Options dictionary is modified by side-effect.
353 def doAction(action,options,exclude={}, output=None):
354 """doAction: execute one action, ie display a few message, call the right
355 find* function, and then call doCleanup with its result."""
356 # define vocabulary for the output
357 if action == 'packages':
358 files_type = "binary packages"
360 files_type = "distfiles"
363 # find files to delete, depending on the action
364 if not options['quiet']:
365 output.einfo("Building file list for "+action+" cleaning...")
366 if action == 'packages':
367 clean_me = findPackages(
370 destructive=options['destructive'],
371 package_names=options['package-names'],
372 time_limit=options['time-limit'],
374 #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
375 #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
379 engine = DistfilesSearch(output=options['verbose-output'],
380 #portdb=Dbapi(portage.db[portage.root]["porttree"].dbapi),
381 #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
383 clean_me, saved, deprecated = engine.findDistfiles(
385 destructive=options['destructive'],
386 fetch_restricted=options['fetch-restricted'],
387 package_names=options['package-names'],
388 time_limit=options['time-limit'],
389 size_limit=options['size-limit'],
390 deprecate = options['deprecated']
392 # actually clean files if something was found
394 # verbose pretend message
395 if options['pretend'] and not options['quiet']:
396 output.einfo("Here are the "+files_type+" that would be deleted:")
397 # verbose non-pretend message
398 elif not options['quiet']:
399 output.einfo("Cleaning " + files_type +"...")
400 # do the cleanup, and get size of deleted files
401 cleaner = CleanUp( output.progress_controller)
402 if options['pretend']:
403 clean_size = cleaner.pretend_clean(clean_me)
404 elif action in ['distfiles']:
405 clean_size = cleaner.clean_dist(clean_me)
406 elif action in ['packages']:
407 clean_size = cleaner.clean_pkgs(clean_me,
409 # vocabulary for final message
410 if options['pretend']:
414 # display freed space
415 if not options['quiet']:
416 output.total('normal', clean_size, len(clean_me), verb, action)
417 # nothing was found, return
418 elif not options['quiet']:
419 output.einfo("Your "+action+" directory was already clean.")
420 if saved and not options['quiet']:
422 print( (pp.emph(" The folowing ") + yellow("Deprecated") +
423 pp.emph(" files were saved from cleaning due to exclusion file entries")))
424 output.set_colors('deprecated')
425 clean_size = cleaner.pretend_clean(saved)
426 output.total('deprecated', clean_size, len(saved), verb, action)
427 if deprecated and not options['quiet']:
429 print( (pp.emph(" The folowing ") + yellow("Deprecated") +
430 pp.emph(" installed packages were found")))
431 output.set_colors('deprecated')
432 output.list_pkgs(deprecated)
436 """Parse command line and execute all actions."""
437 # set default options
439 options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
440 or not sys.stdout.isatty())
441 if options['nocolor']:
443 # parse command line options and actions
445 action = parseArgs(options)
446 # filter exception to know what message to display
447 except ParseArgsException as e:
448 if e.value == 'help':
449 printUsage(help='all')
451 elif e.value[:5] == 'help-':
452 printUsage(help=e.value[5:])
454 elif e.value == 'version':
460 output = OutputControl(options)
461 options['verbose-output'] = lambda x: None
462 if not options['quiet']:
463 if options['verbose']:
464 options['verbose-output'] = output.einfo
465 # parse the exclusion file
466 if 'exclude-file' in options:
468 exclude = parseExcludeFile(options['exclude-file'],
469 options['verbose-output'])
470 except ParseExcludeFileException as e:
471 print( pp.error(str(e)), file=sys.stderr)
473 "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
475 "See format of this file in `man %s`" % __productname__), file=sys.stderr)
478 exclude_file = "/etc/%s/%s.exclude" % (__productname__ , action)
479 if os.path.isfile(exclude_file):
480 options['exclude-file'] = exclude_file
482 # security check for non-pretend mode
483 if not options['pretend'] and portage.secpass == 0:
485 "Permission denied: you must be root or belong to " +
486 "the portage group."), file=sys.stderr)
489 doAction(action, options, exclude=exclude,
493 if __name__ == "__main__":
494 """actually call main() if launched as a script"""
497 except KeyboardInterrupt: