Change the documentation for revdep-rebuild --library.
[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 os
20 import sys
21 import re
22 import time
23 import getopt
24
25 import portage
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 from gentoolkit.eprefix import EPREFIX
37
38 def printVersion():
39         """Output the version info."""
40         print( "%s (%s) - %s" \
41                         % (__productname__, __version__, __description__))
42         print()
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")
46
47
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."""
51
52         out = sys.stdout
53         if _error:
54                 out = sys.stderr
55         if not _error in ('actions', 'global-options', \
56                         'packages-options', 'distfiles-options', \
57                         'merged-packages-options', 'merged-distfiles-options', \
58                         'time', 'size'):
59                 _error = None
60         if not _error and not help: help = 'all'
61         if _error == 'time':
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)
69                 return
70         if _error == 'size':
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)
77                 return
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)
81                 print( file=out)
82         elif _error == 'actions':
83                 print( pp.error("Wrong or missing action name on command line."), file=out)
84                 print( 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] ..."),
90                         green("<action>"),
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)
101         if help == 'all':
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)
110         print(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)
138                 print( 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)
145                 print( 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)
151                 print( 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)
164                 print( file=out)
165         print( "More detailed instruction can be found in",
166                         turquoise("`man %s`" % __productname__), file=out)
167
168
169 class ParseArgsException(Exception):
170         """For parseArgs() -> main() communications."""
171         def __init__(self, value):
172                 self.value = value # sdfgsdfsdfsd
173         def __str__(self):
174                 return repr(self.value)
175
176
177 def parseSize(size):
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).
180
181         @raise ParseArgsException: in case of failure
182         """
183         units = {
184                 'G': (1024**3),
185                 'M': (1024**2),
186                 'K': 1024,
187                 'B': 1
188         }
189         try:
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()]
194         except:
195                 raise ParseArgsException('size')
196         return size
197
198
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?).
204         """
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
210         try:
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()
216         except:
217                 raise ParseArgsException('time')
218         return time.time() - (value * units[unit])
219
220
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
224         the options dict.
225         """
226
227         def optionSwitch(option,opts,action=None):
228                 """local function for interpreting command line options
229                 and setting options accordingly"""
230                 return_code = True
231                 for o, a in opts:
232                         if o in ("-h", "--help"):
233                                 if action:
234                                         raise ParseArgsException('help-'+action)
235                                 else:
236                                         raise ParseArgsException('help')
237                         elif o in ("-V", "--version"):
238                                 raise ParseArgsException('version')
239                         elif o in ("-C", "--nocolor"):
240                                 options['nocolor'] = True
241                                 pp.output.nocolor()
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
267                         else:
268                                 return_code = False
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']:
273                                         print( pp.error(
274                                                 "--%s only makes sense in --destructive mode." % opt), file=sys.stderr)
275                                 options[opt] = False
276                 return return_code
277
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:
301         action = None
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'):
306                 action = 'packages'
307         elif os.path.basename(sys.argv[0]) in \
308                         (__productname__+'-dist', __productname__+'-distfiles'):
309                 action = 'distfiles'
310         # prepare for the first getopt
311         if action:
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
317         else:
318                 short_opts = getopt_options['short']['global']
319                 long_opts = getopt_options['long']['global']
320                 opts_mode = 'global'
321         # apply getopts to command line, show partial help on failure
322         try:
323                 opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
324         except:
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
332         if action:
333                 return action
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')
338         action = args.pop(0)
339         # parse the action specific options
340         try:
341                 opts, args = getopt.getopt(args, \
342                         getopt_options['short'][action], \
343                         getopt_options['long'][action])
344         except:
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!
349         if len(args):
350                 raise ParseArgsException(action+'-options')
351         # returns the action. Options dictionary is modified by side-effect.
352         return action
353
354
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"
361         else:
362                 files_type = "distfiles"
363         saved = {}
364         deprecated = {}
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(
370                         options,
371                         exclude=exclude,
372                         destructive=options['destructive'],
373                         package_names=options['package-names'],
374                         time_limit=options['time-limit'],
375                         pkgdir=pkgdir,
376                         #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
377                         #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
378                 )
379         else:
380                 # accept defaults
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),
384                 )
385                 clean_me, saved, deprecated = engine.findDistfiles(
386                         exclude=exclude,
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']
393                 )
394         # actually clean files if something was found
395         if clean_me:
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,
410                                 pkgdir)
411                 # vocabulary for final message
412                 if options['pretend']:
413                         verb = "would be"
414                 else:
415                         verb = "were"
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']:
423                 print()
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']:
430                 print()
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)
435
436
437 def main():
438         """Parse command line and execute all actions."""
439         # set default options
440         options = {}
441         options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
442                 or not sys.stdout.isatty())
443         if options['nocolor']:
444                 pp.output.nocolor()
445         # parse command line options and actions
446         try:
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')
452                         sys.exit(0)
453                 elif e.value[:5] == 'help-':
454                         printUsage(help=e.value[5:])
455                         sys.exit(0)
456                 elif e.value == 'version':
457                         printVersion()
458                         sys.exit(0)
459                 else:
460                         printUsage(e.value)
461                         sys.exit(2)
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:
474                 try:
475                         exclude = parseExcludeFile(options['exclude-file'],
476                                         options['verbose-output'])
477                 except ParseExcludeFileException as e:
478                         print( pp.error(str(e)), file=sys.stderr)
479                         print( pp.error(
480                                 "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
481                         print( pp.error(
482                                 "See format of this file in `man %s`" % __productname__), file=sys.stderr)
483                         sys.exit(1)
484         else:
485                         exclude = {}
486         # security check for non-pretend mode
487         if not options['pretend'] and portage.secpass == 0:
488                 print( pp.error(
489                         "Permission denied: you must be root or belong to " +
490                         "the portage group."), file=sys.stderr)
491                 sys.exit(1)
492         # execute action
493         doAction(action, options, exclude=exclude,
494                 output=output)
495
496
497 if __name__ == "__main__":
498         """actually call main() if launched as a script"""
499         try:
500                 main()
501         except KeyboardInterrupt:
502                 print( "Aborted.")
503                 sys.exit(130)
504         sys.exit(0)