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"""
233 if o in ("-h", "--help"):
235 elif o in ("-V", "--version"):
236 raise ParseArgsException('version')
237 elif o in ("-C", "--nocolor"):
238 options['nocolor'] = True
240 elif o in ("-d", "--destructive"):
241 options['destructive'] = True
242 elif o in ("-D", "--deprecated"):
243 options['deprecated'] = True
244 elif o in ("-i", "--interactive") and not options['pretend']:
245 options['interactive'] = True
246 elif o in ("-p", "--pretend"):
247 options['pretend'] = True
248 options['interactive'] = False
249 elif o in ("-q", "--quiet"):
250 options['quiet'] = True
251 options['verbose'] = False
252 elif o in ("-t", "--time-limit"):
253 options['time-limit'] = parseTime(a)
254 elif o in ("-e", "--exclude-file"):
255 print("cli --exclude option")
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 raise ParseArgsException('help-'+action)
278 raise ParseArgsException('help')
281 # here are the different allowed command line options (getopt args)
282 getopt_options = {'short':{}, 'long':{}}
283 getopt_options['short']['global'] = "CdDipqe:t:nhVv"
284 getopt_options['long']['global'] = ["nocolor", "destructive",
285 "deprecated", "interactive", "pretend", "quiet", "exclude-file=",
286 "time-limit=", "package-names", "help", "version", "verbose"]
287 getopt_options['short']['distfiles'] = "fs:"
288 getopt_options['long']['distfiles'] = ["fetch-restricted", "size-limit="]
289 getopt_options['short']['packages'] = ""
290 getopt_options['long']['packages'] = [""]
291 # set default options, except 'nocolor', which is set in main()
292 options['interactive'] = False
293 options['pretend'] = False
294 options['quiet'] = False
295 options['accept_all'] = False
296 options['destructive'] = False
297 options['deprecated'] = False
298 options['time-limit'] = 0
299 options['package-names'] = False
300 options['fetch-restricted'] = False
301 options['size-limit'] = 0
302 options['verbose'] = False
303 # if called by a well-named symlink, set the acction accordingly:
305 # temp print line to ensure it is the svn/branch code running, etc..
306 #print( "###### svn/branch/gentoolkit_eclean ####### ==> ", os.path.basename(sys.argv[0]))
307 if os.path.basename(sys.argv[0]) in \
308 (__productname__+'-pkg', __productname__+'-packages'):
310 elif os.path.basename(sys.argv[0]) in \
311 (__productname__+'-dist', __productname__+'-distfiles'):
313 # prepare for the first getopt
315 short_opts = getopt_options['short']['global'] \
316 + getopt_options['short'][action]
317 long_opts = getopt_options['long']['global'] \
318 + getopt_options['long'][action]
319 opts_mode = 'merged-'+action
321 short_opts = getopt_options['short']['global']
322 long_opts = getopt_options['long']['global']
324 # apply getopts to command line, show partial help on failure
326 opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
328 raise ParseArgsException(opts_mode+'-options')
329 # set options accordingly
330 optionSwitch(options,opts,action=action)
331 # if action was already set, there should be no more args
332 if action and len(args):
333 raise ParseArgsException(opts_mode+'-options')
334 # if action was set, there is nothing left to do
337 # So, we are in "eclean --foo action --bar" mode. Parse remaining args...
338 # Only two actions are allowed: 'packages' and 'distfiles'.
339 if not len(args) or not args[0] in ('packages','distfiles'):
340 raise ParseArgsException('actions')
342 # parse the action specific options
344 opts, args = getopt.getopt(args, \
345 getopt_options['short'][action], \
346 getopt_options['long'][action])
348 raise ParseArgsException(action+'-options')
349 # set options again, for action-specific options
350 optionSwitch(options,opts,action=action)
351 # any remaning args? Then die!
353 raise ParseArgsException(action+'-options')
354 # returns the action. Options dictionary is modified by side-effect.
358 def doAction(action,options,exclude={}, output=None):
359 """doAction: execute one action, ie display a few message, call the right
360 find* function, and then call doCleanup with its result."""
361 # define vocabulary for the output
362 if action == 'packages':
363 files_type = "binary packages"
365 files_type = "distfiles"
368 # find files to delete, depending on the action
369 if not options['quiet']:
370 output.einfo("Building file list for "+action+" cleaning...")
371 if action == 'packages':
372 clean_me = findPackages(
375 destructive=options['destructive'],
376 package_names=options['package-names'],
377 time_limit=options['time-limit'],
379 #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
380 #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
384 engine = DistfilesSearch(output=options['verbose-output'],
385 #portdb=Dbapi(portage.db[portage.root]["porttree"].dbapi),
386 #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
388 clean_me, saved, deprecated = engine.findDistfiles(
390 destructive=options['destructive'],
391 fetch_restricted=options['fetch-restricted'],
392 package_names=options['package-names'],
393 time_limit=options['time-limit'],
394 size_limit=options['size-limit'],
395 deprecate = options['deprecated']
397 # actually clean files if something was found
399 # verbose pretend message
400 if options['pretend'] and not options['quiet']:
401 output.einfo("Here are the "+files_type+" that would be deleted:")
402 # verbose non-pretend message
403 elif not options['quiet']:
404 output.einfo("Cleaning " + files_type +"...")
405 # do the cleanup, and get size of deleted files
406 cleaner = CleanUp( output.progress_controller)
407 if options['pretend']:
408 clean_size = cleaner.pretend_clean(clean_me)
409 elif action in ['distfiles']:
410 clean_size = cleaner.clean_dist(clean_me)
411 elif action in ['packages']:
412 clean_size = cleaner.clean_pkgs(clean_me,
414 # vocabulary for final message
415 if options['pretend']:
419 # display freed space
420 if not options['quiet']:
421 output.total('normal', clean_size, len(clean_me), verb, action)
422 # nothing was found, return
423 elif not options['quiet']:
424 output.einfo("Your "+action+" directory was already clean.")
425 if saved and not options['quiet']:
427 print( (pp.emph(" The following ") + yellow("unavailable") +
428 pp.emph(" files were saved from cleaning due to exclusion file entries")))
429 output.set_colors('deprecated')
430 clean_size = cleaner.pretend_clean(saved)
431 output.total('deprecated', clean_size, len(saved), verb, action)
432 if deprecated and not options['quiet']:
434 print( (pp.emph(" The following ") + yellow("unavailable") +
435 pp.emph(" installed packages were found")))
436 output.set_colors('deprecated')
437 output.list_pkgs(deprecated)
441 """Parse command line and execute all actions."""
442 # set default options
444 options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
445 or not sys.stdout.isatty())
446 if options['nocolor']:
448 # parse command line options and actions
450 action = parseArgs(options)
451 # filter exception to know what message to display
452 except ParseArgsException as e:
453 if e.value == 'help':
454 printUsage(help='all')
456 elif e.value[:5] == 'help-':
457 printUsage(help=e.value[5:])
459 elif e.value == 'version':
465 output = OutputControl(options)
466 options['verbose-output'] = lambda x: None
467 if not options['quiet']:
468 if options['verbose']:
469 options['verbose-output'] = output.einfo
470 # parse the exclusion file
471 if not 'exclude-file' in options:
472 # set it to the default exclude file if it exists
473 exclude_file = "%s/etc/%s/%s.exclude" % (EPREFIX,__productname__ , action)
474 if os.path.isfile(exclude_file):
475 options['exclude-file'] = exclude_file
476 if 'exclude-file' in options:
478 exclude = parseExcludeFile(options['exclude-file'],
479 options['verbose-output'])
480 except ParseExcludeFileException as e:
481 print( pp.error(str(e)), file=sys.stderr)
483 "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
485 "See format of this file in `man %s`" % __productname__), file=sys.stderr)
489 # security check for non-pretend mode
490 if not options['pretend'] and portage.secpass == 0:
492 "Permission denied: you must be root or belong to " +
493 "the portage group."), file=sys.stderr)
496 doAction(action, options, exclude=exclude,
500 if __name__ == "__main__":
501 """actually call main() if launched as a script"""
504 except KeyboardInterrupt: