Merge branch 'gentoolkit' of git+ssh://git.overlays.gentoo.org/proj/gentoolkit into...
[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                 do_help = False
232                 for o, a in opts:
233                         if o in ("-h", "--help"):
234                                 do_help = True
235                         elif o in ("-V", "--version"):
236                                 raise ParseArgsException('version')
237                         elif o in ("-C", "--nocolor"):
238                                 options['nocolor'] = True
239                                 pp.output.nocolor()
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
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                 if do_help:
275                         if action:
276                                 raise ParseArgsException('help-'+action)
277                         else:
278                                 raise ParseArgsException('help')
279                 return return_code
280
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:
304         action = None
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'):
309                 action = 'packages'
310         elif os.path.basename(sys.argv[0]) in \
311                         (__productname__+'-dist', __productname__+'-distfiles'):
312                 action = 'distfiles'
313         # prepare for the first getopt
314         if action:
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
320         else:
321                 short_opts = getopt_options['short']['global']
322                 long_opts = getopt_options['long']['global']
323                 opts_mode = 'global'
324         # apply getopts to command line, show partial help on failure
325         try:
326                 opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
327         except:
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
335         if action:
336                 return action
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')
341         action = args.pop(0)
342         # parse the action specific options
343         try:
344                 opts, args = getopt.getopt(args, \
345                         getopt_options['short'][action], \
346                         getopt_options['long'][action])
347         except:
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!
352         if len(args):
353                 raise ParseArgsException(action+'-options')
354         # returns the action. Options dictionary is modified by side-effect.
355         return action
356
357
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"
364         else:
365                 files_type = "distfiles"
366         saved = {}
367         deprecated = {}
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(
373                         options,
374                         exclude=exclude,
375                         destructive=options['destructive'],
376                         package_names=options['package-names'],
377                         time_limit=options['time-limit'],
378                         pkgdir=pkgdir,
379                         #port_dbapi=Dbapi(portage.db[portage.root]["porttree"].dbapi),
380                         #var_dbapi=Dbapi(portage.db[portage.root]["vartree"].dbapi),
381                 )
382         else:
383                 # accept defaults
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),
387                 )
388                 clean_me, saved, deprecated = engine.findDistfiles(
389                         exclude=exclude,
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']
396                 )
397         # actually clean files if something was found
398         if clean_me:
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,
413                                 pkgdir)
414                 # vocabulary for final message
415                 if options['pretend']:
416                         verb = "would be"
417                 else:
418                         verb = "were"
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']:
426                 print()
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']:
433                 print()
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)
438
439
440 def main():
441         """Parse command line and execute all actions."""
442         # set default options
443         options = {}
444         options['nocolor'] = (port_settings["NOCOLOR"] in ('yes','true')
445                 or not sys.stdout.isatty())
446         if options['nocolor']:
447                 pp.output.nocolor()
448         # parse command line options and actions
449         try:
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')
455                         sys.exit(0)
456                 elif e.value[:5] == 'help-':
457                         printUsage(help=e.value[5:])
458                         sys.exit(0)
459                 elif e.value == 'version':
460                         printVersion()
461                         sys.exit(0)
462                 else:
463                         printUsage(e.value)
464                         sys.exit(2)
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:
477                 try:
478                         exclude = parseExcludeFile(options['exclude-file'],
479                                         options['verbose-output'])
480                 except ParseExcludeFileException as e:
481                         print( pp.error(str(e)), file=sys.stderr)
482                         print( pp.error(
483                                 "Invalid exclusion file: %s" % options['exclude-file']), file=sys.stderr)
484                         print( pp.error(
485                                 "See format of this file in `man %s`" % __productname__), file=sys.stderr)
486                         sys.exit(1)
487         else:
488                         exclude = {}
489         # security check for non-pretend mode
490         if not options['pretend'] and portage.secpass == 0:
491                 print( pp.error(
492                         "Permission denied: you must be root or belong to " +
493                         "the portage group."), file=sys.stderr)
494                 sys.exit(1)
495         # execute action
496         doAction(action, options, exclude=exclude,
497                 output=output)
498
499
500 if __name__ == "__main__":
501         """actually call main() if launched as a script"""
502         try:
503                 main()
504         except KeyboardInterrupt:
505                 print( "Aborted.")
506                 sys.exit(130)
507         sys.exit(0)