Update to genscripts rev 382. This has more fixes for py3k and the modular rewrite...
[gentoolkit.git] / pym / gentoolkit / eclean / cli.py
1 #!/usr/bin/python
2
3 # Copyright 2003-2010 Gentoo Foundation
4 # Distributed under the terms of the GNU General Public License v2
5
6
7 from __future__ import print_function
8
9
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"
14 __version__ = "svn"
15 __productname__ = "eclean"
16 __description__ = "A cleaning tool for Gentoo distfiles and binaries."
17
18
19 import sys
20 import re
21 import time
22 import getopt
23
24 import portage
25 from portage import os
26 from portage.output import white, yellow, turquoise, green, teal, red
27
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
37 def printVersion():
38         """Output the version info."""
39         print( "%s (%s) - %s" \
40                         % (__productname__, __version__, __description__))
41         print()
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")
45
46
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."""
50
51         out = sys.stdout
52         if _error:
53                 out = sys.stderr
54         if not _error in ('actions', 'global-options', \
55                         'packages-options', 'distfiles-options', \
56                         'merged-packages-options', 'merged-distfiles-options', \
57                         'time', 'size'):
58                 _error = None
59         if not _error and not help: help = 'all'
60         if _error == 'time':
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)
68                 return
69         if _error == 'size':
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)
76                 return
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)
80                 print( file=out)
81         elif _error == 'actions':
82                 print( pp.error("Wrong or missing action name on command line."), file=out)
83                 print( 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] ..."),
89                         green("<action>"),
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)
100         if help == 'all':
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)
109         print(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)
137                 print( 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)
144                 print( 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)
150                 print( 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)
163                 print( file=out)
164         print( "More detailed instruction can be found in",
165                         turquoise("`man %s`" % __productname__), file=out)
166
167
168 class ParseArgsException(Exception):
169         """For parseArgs() -> main() communications."""
170         def __init__(self, value):
171                 self.value = value # sdfgsdfsdfsd
172         def __str__(self):
173                 return repr(self.value)
174
175
176 def parseSize(size):
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).
179
180         @raise ParseArgsException: in case of failure
181         """
182         units = {
183                 'G': (1024**3),
184                 'M': (1024**2),
185                 'K': 1024,
186                 'B': 1
187         }
188         try:
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()]
193         except:
194                 raise ParseArgsException('size')
195         return size
196
197
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?).
203         """
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
209         try:
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()
215         except:
216                 raise ParseArgsException('time')
217         return time.time() - (value * units[unit])
218
219
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
223         the options dict.
224         """
225
226         def optionSwitch(option,opts,action=None):
227                 """local function for interpreting command line options
228                 and setting options accordingly"""
229                 return_code = True
230                 for o, a in opts:
231                         if o in ("-h", "--help"):
232                                 if action:
233                                         raise ParseArgsException('help-'+action)
234                                 else:
235                                         raise ParseArgsException('help')
236                         elif o in ("-V", "--version"):
237                                 raise ParseArgsException('version')
238                         elif o in ("-C", "--nocolor"):
239                                 options['nocolor'] = True
240                                 pp.output.nocolor()
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
265                         else:
266                                 return_code = False
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']:
271                                         print( pp.error(
272                                                 "--%s only makes sense in --destructive mode." % opt), file=sys.stderr)
273                                 options[opt] = False
274                 return return_code
275
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:
299         action = None
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'):
304                 action = 'packages'
305         elif os.path.basename(sys.argv[0]) in \
306                         (__productname__+'-dist', __productname__+'-distfiles'):
307                 action = 'distfiles'
308         # prepare for the first getopt
309         if action:
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
315         else:
316                 short_opts = getopt_options['short']['global']
317                 long_opts = getopt_options['long']['global']
318                 opts_mode = 'global'
319         # apply getopts to command line, show partial help on failure
320         try:
321                 opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
322         except:
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
330         if action:
331                 return action
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')
336         action = args.pop(0)
337         # parse the action specific options
338         try:
339                 opts, args = getopt.getopt(args, \
340                         getopt_options['short'][action], \
341                         getopt_options['long'][action])
342         except:
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!
347         if len(args):
348                 raise ParseArgsException(action+'-options')
349         # returns the action. Options dictionary is modified by side-effect.
350         return action
351
352
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"
359         else:
360                 files_type = "distfiles"
361         saved = {}
362         deprecated = {}
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(
368                         options,
369                         exclude=exclude,
370                         destructive=options['destructive'],
371                         package_names=options['package-names'],
372                         time_limit=options['time-limit'],
373                         pkgdir=pkgdir,
374                         #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
375                         #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
376                 )
377         else:
378                 # accept defaults
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),
382                 )
383                 clean_me, saved, deprecated = engine.findDistfiles(
384                         exclude=exclude,
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']
391                 )
392         # actually clean files if something was found
393         if clean_me:
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,
408                                 pkgdir)
409                 # vocabulary for final message
410                 if options['pretend']:
411                         verb = "would be"
412                 else:
413                         verb = "were"
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']:
421                 print()
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']:
428                 print()
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)
433
434
435 def main():
436         """Parse command line and execute all actions."""
437         # set default options
438         options = {}
439         options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
440                 or not sys.stdout.isatty())
441         if options['nocolor']:
442                 pp.output.nocolor()
443         # parse command line options and actions
444         try:
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')
450                         sys.exit(0)
451                 elif e.value[:5] == 'help-':
452                         printUsage(help=e.value[5:])
453                         sys.exit(0)
454                 elif e.value == 'version':
455                         printVersion()
456                         sys.exit(0)
457                 else:
458                         printUsage(e.value)
459                         sys.exit(2)
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:
467                 try:
468                         exclude = parseExcludeFile(options['exclude-file'],
469                                         options['verbose-output'])
470                 except ParseExcludeFileException as e:
471                         print( pp.error(str(e)), file=sys.stderr)
472                         print( pp.error(
473                                 "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
474                         print( pp.error(
475                                 "See format of this file in `man %s`" % __productname__), file=sys.stderr)
476                         sys.exit(1)
477         else:
478                 exclude_file = "/etc/%s/%s.exclude" % (__productname__ , action)
479                 if os.path.isfile(exclude_file):
480                         options['exclude-file'] = exclude_file
481                 exclude={}
482         # security check for non-pretend mode
483         if not options['pretend'] and portage.secpass == 0:
484                 print( pp.error(
485                         "Permission denied: you must be root or belong to " +
486                         "the portage group."), file=sys.stderr)
487                 sys.exit(1)
488         # execute action
489         doAction(action, options, exclude=exclude,
490                 output=output)
491
492
493 if __name__ == "__main__":
494         """actually call main() if launched as a script"""
495         try:
496                 main()
497         except KeyboardInterrupt:
498                 print( "Aborted.")
499                 sys.exit(130)
500         sys.exit(0)