Update scons to 2.3.1
[scons.git] / scons-time.py
1 #!/usr/bin/env python
2 #
3 # scons-time - run SCons timings and collect statistics
4 #
5 # A script for running a configuration through SCons with a standard
6 # set of invocations to collect timing and memory statistics and to
7 # capture the results in a consistent set of output files for display
8 # and analysis.
9 #
10
11 #
12 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 The SCons Foundation
13 #
14 # Permission is hereby granted, free of charge, to any person obtaining
15 # a copy of this software and associated documentation files (the
16 # "Software"), to deal in the Software without restriction, including
17 # without limitation the rights to use, copy, modify, merge, publish,
18 # distribute, sublicense, and/or sell copies of the Software, and to
19 # permit persons to whom the Software is furnished to do so, subject to
20 # the following conditions:
21 #
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
24 #
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
27 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 from __future__ import division
33 from __future__ import nested_scopes
34
35 __revision__ = "src/script/scons-time.py  2014/03/02 14:18:15 garyo"
36
37 import getopt
38 import glob
39 import os
40 import re
41 import shutil
42 import sys
43 import tempfile
44 import time
45
46 try:
47     sorted
48 except NameError:
49     # Pre-2.4 Python has no sorted() function.
50     #
51     # The pre-2.4 Python list.sort() method does not support
52     # list.sort(key=) nor list.sort(reverse=) keyword arguments, so
53     # we must implement the functionality of those keyword arguments
54     # by hand instead of passing them to list.sort().
55     def sorted(iterable, cmp=None, key=None, reverse=False):
56         if key is not None:
57             result = [(key(x), x) for x in iterable]
58         else:
59             result = iterable[:]
60         if cmp is None:
61             # Pre-2.3 Python does not support list.sort(None).
62             result.sort()
63         else:
64             result.sort(cmp)
65         if key is not None:
66             result = [t1 for t0,t1 in result]
67         if reverse:
68             result.reverse()
69         return result
70
71 if os.environ.get('SCONS_HORRIBLE_REGRESSION_TEST_HACK') is not None:
72     # We can't apply the 'callable' fixer until the floor is 2.6, but the
73     # '-3' option to Python 2.6 and 2.7 generates almost ten thousand
74     # warnings.  This hack allows us to run regression tests with the '-3'
75     # option by replacing the callable() built-in function with a hack
76     # that performs the same function but doesn't generate the warning.
77     # Note that this hack is ONLY intended to be used for regression
78     # testing, and should NEVER be used for real runs.
79     from types import ClassType
80     def callable(obj):
81         if hasattr(obj, '__call__'): return True
82         if isinstance(obj, (ClassType, type)): return True
83         return False
84
85 def make_temp_file(**kw):
86     try:
87         result = tempfile.mktemp(**kw)
88         try:
89             result = os.path.realpath(result)
90         except AttributeError:
91             # Python 2.1 has no os.path.realpath() method.
92             pass
93     except TypeError:
94         try:
95             save_template = tempfile.template
96             prefix = kw['prefix']
97             del kw['prefix']
98             tempfile.template = prefix
99             result = tempfile.mktemp(**kw)
100         finally:
101             tempfile.template = save_template
102     return result
103
104 def HACK_for_exec(cmd, *args):
105     '''
106     For some reason, Python won't allow an exec() within a function
107     that also declares an internal function (including lambda functions).
108     This function is a hack that calls exec() in a function with no
109     internal functions.
110     '''
111     if not args:          exec(cmd)
112     elif len(args) == 1:  exec cmd in args[0]
113     else:                 exec cmd in args[0], args[1]
114
115 class Plotter(object):
116     def increment_size(self, largest):
117         """
118         Return the size of each horizontal increment line for a specified
119         maximum value.  This returns a value that will provide somewhere
120         between 5 and 9 horizontal lines on the graph, on some set of
121         boundaries that are multiples of 10/100/1000/etc.
122         """
123         i = largest // 5
124         if not i:
125             return largest
126         multiplier = 1
127         while i >= 10:
128             i = i // 10
129             multiplier = multiplier * 10
130         return i * multiplier
131
132     def max_graph_value(self, largest):
133         # Round up to next integer.
134         largest = int(largest) + 1
135         increment = self.increment_size(largest)
136         return ((largest + increment - 1) // increment) * increment
137
138 class Line(object):
139     def __init__(self, points, type, title, label, comment, fmt="%s %s"):
140         self.points = points
141         self.type = type
142         self.title = title
143         self.label = label
144         self.comment = comment
145         self.fmt = fmt
146
147     def print_label(self, inx, x, y):
148         if self.label:
149             print 'set label %s "%s" at %s,%s right' % (inx, self.label, x, y)
150
151     def plot_string(self):
152         if self.title:
153             title_string = 'title "%s"' % self.title
154         else:
155             title_string = 'notitle'
156         return "'-' %s with lines lt %s" % (title_string, self.type)
157
158     def print_points(self, fmt=None):
159         if fmt is None:
160             fmt = self.fmt
161         if self.comment:
162             print '# %s' % self.comment
163         for x, y in self.points:
164             # If y is None, it usually represents some kind of break
165             # in the line's index number.  We might want to represent
166             # this some way rather than just drawing the line straight
167             # between the two points on either side.
168             if not y is None:
169                 print fmt % (x, y)
170         print 'e'
171
172     def get_x_values(self):
173         return [ p[0] for p in self.points ]
174
175     def get_y_values(self):
176         return [ p[1] for p in self.points ]
177
178 class Gnuplotter(Plotter):
179
180     def __init__(self, title, key_location):
181         self.lines = []
182         self.title = title
183         self.key_location = key_location
184
185     def line(self, points, type, title=None, label=None, comment=None, fmt='%s %s'):
186         if points:
187             line = Line(points, type, title, label, comment, fmt)
188             self.lines.append(line)
189
190     def plot_string(self, line):
191         return line.plot_string()
192
193     def vertical_bar(self, x, type, label, comment):
194         if self.get_min_x() <= x and x <= self.get_max_x():
195             points = [(x, 0), (x, self.max_graph_value(self.get_max_y()))]
196             self.line(points, type, label, comment)
197
198     def get_all_x_values(self):
199         result = []
200         for line in self.lines:
201             result.extend(line.get_x_values())
202         return [r for r in result if not r is None]
203
204     def get_all_y_values(self):
205         result = []
206         for line in self.lines:
207             result.extend(line.get_y_values())
208         return [r for r in result if not r is None]
209
210     def get_min_x(self):
211         try:
212             return self.min_x
213         except AttributeError:
214             try:
215                 self.min_x = min(self.get_all_x_values())
216             except ValueError:
217                 self.min_x = 0
218             return self.min_x
219
220     def get_max_x(self):
221         try:
222             return self.max_x
223         except AttributeError:
224             try:
225                 self.max_x = max(self.get_all_x_values())
226             except ValueError:
227                 self.max_x = 0
228             return self.max_x
229
230     def get_min_y(self):
231         try:
232             return self.min_y
233         except AttributeError:
234             try:
235                 self.min_y = min(self.get_all_y_values())
236             except ValueError:
237                 self.min_y = 0
238             return self.min_y
239
240     def get_max_y(self):
241         try:
242             return self.max_y
243         except AttributeError:
244             try:
245                 self.max_y = max(self.get_all_y_values())
246             except ValueError:
247                 self.max_y = 0
248             return self.max_y
249
250     def draw(self):
251
252         if not self.lines:
253             return
254
255         if self.title:
256             print 'set title "%s"' % self.title
257         print 'set key %s' % self.key_location
258
259         min_y = self.get_min_y()
260         max_y = self.max_graph_value(self.get_max_y())
261         incr = (max_y - min_y) / 10.0
262         start = min_y + (max_y / 2.0) + (2.0 * incr)
263         position = [ start - (i * incr) for i in range(5) ]
264
265         inx = 1
266         for line in self.lines:
267             line.print_label(inx, line.points[0][0]-1,
268                              position[(inx-1) % len(position)])
269             inx += 1
270
271         plot_strings = [ self.plot_string(l) for l in self.lines ]
272         print 'plot ' + ', \\\n     '.join(plot_strings)
273
274         for line in self.lines:
275             line.print_points()
276
277
278
279 def untar(fname):
280     import tarfile
281     tar = tarfile.open(name=fname, mode='r')
282     for tarinfo in tar:
283         tar.extract(tarinfo)
284     tar.close()
285
286 def unzip(fname):
287     import zipfile
288     zf = zipfile.ZipFile(fname, 'r')
289     for name in zf.namelist():
290         dir = os.path.dirname(name)
291         try:
292             os.makedirs(dir)
293         except:
294             pass
295         open(name, 'w').write(zf.read(name))
296
297 def read_tree(dir):
298     for dirpath, dirnames, filenames in os.walk(dir):
299         for fn in filenames:
300             fn = os.path.join(dirpath, fn)
301             if os.path.isfile(fn):
302                 open(fn, 'rb').read()
303
304 def redirect_to_file(command, log):
305     return '%s > %s 2>&1' % (command, log)
306
307 def tee_to_file(command, log):
308     return '%s 2>&1 | tee %s' % (command, log)
309
310
311     
312 class SConsTimer(object):
313     """
314     Usage: scons-time SUBCOMMAND [ARGUMENTS]
315     Type "scons-time help SUBCOMMAND" for help on a specific subcommand.
316
317     Available subcommands:
318         func            Extract test-run data for a function
319         help            Provides help
320         mem             Extract --debug=memory data from test runs
321         obj             Extract --debug=count data from test runs
322         time            Extract --debug=time data from test runs
323         run             Runs a test configuration
324     """
325
326     name = 'scons-time'
327     name_spaces = ' '*len(name)
328
329     def makedict(**kw):
330         return kw
331
332     default_settings = makedict(
333         aegis               = 'aegis',
334         aegis_project       = None,
335         chdir               = None,
336         config_file         = None,
337         initial_commands    = [],
338         key_location        = 'bottom left',
339         orig_cwd            = os.getcwd(),
340         outdir              = None,
341         prefix              = '',
342         python              = '"%s"' % sys.executable,
343         redirect            = redirect_to_file,
344         scons               = None,
345         scons_flags         = '--debug=count --debug=memory --debug=time --debug=memoizer',
346         scons_lib_dir       = None,
347         scons_wrapper       = None,
348         startup_targets     = '--help',
349         subdir              = None,
350         subversion_url      = None,
351         svn                 = 'svn',
352         svn_co_flag         = '-q',
353         tar                 = 'tar',
354         targets             = '',
355         targets0            = None,
356         targets1            = None,
357         targets2            = None,
358         title               = None,
359         unzip               = 'unzip',
360         verbose             = False,
361         vertical_bars       = [],
362
363         unpack_map = {
364             '.tar.gz'       : (untar,   '%(tar)s xzf %%s'),
365             '.tgz'          : (untar,   '%(tar)s xzf %%s'),
366             '.tar'          : (untar,   '%(tar)s xf %%s'),
367             '.zip'          : (unzip,   '%(unzip)s %%s'),
368         },
369     )
370
371     run_titles = [
372         'Startup',
373         'Full build',
374         'Up-to-date build',
375     ]
376
377     run_commands = [
378         '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof0)s %(targets0)s',
379         '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof1)s %(targets1)s',
380         '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof2)s %(targets2)s',
381     ]
382
383     stages = [
384         'pre-read',
385         'post-read',
386         'pre-build',
387         'post-build',
388     ]
389
390     stage_strings = {
391         'pre-read'      : 'Memory before reading SConscript files:',
392         'post-read'     : 'Memory after reading SConscript files:',
393         'pre-build'     : 'Memory before building targets:',
394         'post-build'    : 'Memory after building targets:',
395     }
396
397     memory_string_all = 'Memory '
398
399     default_stage = stages[-1]
400
401     time_strings = {
402         'total'         : 'Total build time',
403         'SConscripts'   : 'Total SConscript file execution time',
404         'SCons'         : 'Total SCons execution time',
405         'commands'      : 'Total command execution time',
406     }
407     
408     time_string_all = 'Total .* time'
409
410     #
411
412     def __init__(self):
413         self.__dict__.update(self.default_settings)
414
415     # Functions for displaying and executing commands.
416
417     def subst(self, x, dictionary):
418         try:
419             return x % dictionary
420         except TypeError:
421             # x isn't a string (it's probably a Python function),
422             # so just return it.
423             return x
424
425     def subst_variables(self, command, dictionary):
426         """
427         Substitutes (via the format operator) the values in the specified
428         dictionary into the specified command.
429
430         The command can be an (action, string) tuple.  In all cases, we
431         perform substitution on strings and don't worry if something isn't
432         a string.  (It's probably a Python function to be executed.)
433         """
434         try:
435             command + ''
436         except TypeError:
437             action = command[0]
438             string = command[1]
439             args = command[2:]
440         else:
441             action = command
442             string = action
443             args = (())
444         action = self.subst(action, dictionary)
445         string = self.subst(string, dictionary)
446         return (action, string, args)
447
448     def _do_not_display(self, msg, *args):
449         pass
450
451     def display(self, msg, *args):
452         """
453         Displays the specified message.
454
455         Each message is prepended with a standard prefix of our name
456         plus the time.
457         """
458         if callable(msg):
459             msg = msg(*args)
460         else:
461             msg = msg % args
462         if msg is None:
463             return
464         fmt = '%s[%s]: %s\n'
465         sys.stdout.write(fmt % (self.name, time.strftime('%H:%M:%S'), msg))
466
467     def _do_not_execute(self, action, *args):
468         pass
469
470     def execute(self, action, *args):
471         """
472         Executes the specified action.
473
474         The action is called if it's a callable Python function, and
475         otherwise passed to os.system().
476         """
477         if callable(action):
478             action(*args)
479         else:
480             os.system(action % args)
481
482     def run_command_list(self, commands, dict):
483         """
484         Executes a list of commands, substituting values from the
485         specified dictionary.
486         """
487         commands = [ self.subst_variables(c, dict) for c in commands ]
488         for action, string, args in commands:
489             self.display(string, *args)
490             sys.stdout.flush()
491             status = self.execute(action, *args)
492             if status:
493                 sys.exit(status)
494
495     def log_display(self, command, log):
496         command = self.subst(command, self.__dict__)
497         if log:
498             command = self.redirect(command, log)
499         return command
500
501     def log_execute(self, command, log):
502         command = self.subst(command, self.__dict__)
503         output = os.popen(command).read()
504         if self.verbose:
505             sys.stdout.write(output)
506         open(log, 'wb').write(output)
507
508     #
509
510     def archive_splitext(self, path):
511         """
512         Splits an archive name into a filename base and extension.
513
514         This is like os.path.splitext() (which it calls) except that it
515         also looks for '.tar.gz' and treats it as an atomic extensions.
516         """
517         if path.endswith('.tar.gz'):
518             return path[:-7], path[-7:]
519         else:
520             return os.path.splitext(path)
521
522     def args_to_files(self, args, tail=None):
523         """
524         Takes a list of arguments, expands any glob patterns, and
525         returns the last "tail" files from the list.
526         """
527         files = []
528         for a in args:
529             files.extend(sorted(glob.glob(a)))
530
531         if tail:
532             files = files[-tail:]
533
534         return files
535
536     def ascii_table(self, files, columns,
537                     line_function, file_function=lambda x: x,
538                     *args, **kw):
539
540         header_fmt = ' '.join(['%12s'] * len(columns))
541         line_fmt = header_fmt + '    %s'
542
543         print header_fmt % columns
544
545         for file in files:
546             t = line_function(file, *args, **kw)
547             if t is None:
548                 t = []
549             diff = len(columns) - len(t)
550             if diff > 0:
551                 t += [''] * diff
552             t.append(file_function(file))
553             print line_fmt % tuple(t)
554
555     def collect_results(self, files, function, *args, **kw):
556         results = {}
557
558         for file in files:
559             base = os.path.splitext(file)[0]
560             run, index = base.split('-')[-2:]
561
562             run = int(run)
563             index = int(index)
564
565             value = function(file, *args, **kw)
566
567             try:
568                 r = results[index]
569             except KeyError:
570                 r = []
571                 results[index] = r
572             r.append((run, value))
573
574         return results
575
576     def doc_to_help(self, obj):
577         """
578         Translates an object's __doc__ string into help text.
579
580         This strips a consistent number of spaces from each line in the
581         help text, essentially "outdenting" the text to the left-most
582         column.
583         """
584         doc = obj.__doc__
585         if doc is None:
586             return ''
587         return self.outdent(doc)
588
589     def find_next_run_number(self, dir, prefix):
590         """
591         Returns the next run number in a directory for the specified prefix.
592
593         Examines the contents the specified directory for files with the
594         specified prefix, extracts the run numbers from each file name,
595         and returns the next run number after the largest it finds.
596         """
597         x = re.compile(re.escape(prefix) + '-([0-9]+).*')
598         matches = [x.match(e) for e in os.listdir(dir)]
599         matches = [_f for _f in matches if _f]
600         if not matches:
601             return 0
602         run_numbers = [int(m.group(1)) for m in matches]
603         return int(max(run_numbers)) + 1
604
605     def gnuplot_results(self, results, fmt='%s %.3f'):
606         """
607         Prints out a set of results in Gnuplot format.
608         """
609         gp = Gnuplotter(self.title, self.key_location)
610
611         for i in sorted(results.keys()):
612             try:
613                 t = self.run_titles[i]
614             except IndexError:
615                 t = '??? %s ???' % i
616             results[i].sort()
617             gp.line(results[i], i+1, t, None, t, fmt=fmt)
618
619         for bar_tuple in self.vertical_bars:
620             try:
621                 x, type, label, comment = bar_tuple
622             except ValueError:
623                 x, type, label = bar_tuple
624                 comment = label
625             gp.vertical_bar(x, type, label, comment)
626
627         gp.draw()
628
629     def logfile_name(self, invocation):
630         """
631         Returns the absolute path of a log file for the specificed
632         invocation number.
633         """
634         name = self.prefix_run + '-%d.log' % invocation
635         return os.path.join(self.outdir, name)
636
637     def outdent(self, s):
638         """
639         Strip as many spaces from each line as are found at the beginning
640         of the first line in the list.
641         """
642         lines = s.split('\n')
643         if lines[0] == '':
644             lines = lines[1:]
645         spaces = re.match(' *', lines[0]).group(0)
646         def strip_initial_spaces(l, s=spaces):
647             if l.startswith(spaces):
648                 l = l[len(spaces):]
649             return l
650         return '\n'.join([ strip_initial_spaces(l) for l in lines ]) + '\n'
651
652     def profile_name(self, invocation):
653         """
654         Returns the absolute path of a profile file for the specified
655         invocation number.
656         """
657         name = self.prefix_run + '-%d.prof' % invocation
658         return os.path.join(self.outdir, name)
659
660     def set_env(self, key, value):
661         os.environ[key] = value
662
663     #
664
665     def get_debug_times(self, file, time_string=None):
666         """
667         Fetch times from the --debug=time strings in the specified file.
668         """
669         if time_string is None:
670             search_string = self.time_string_all
671         else:
672             search_string = time_string
673         contents = open(file).read()
674         if not contents:
675             sys.stderr.write('file %s has no contents!\n' % repr(file))
676             return None
677         result = re.findall(r'%s: ([\d\.]*)' % search_string, contents)[-4:]
678         result = [ float(r) for r in result ]
679         if not time_string is None:
680             try:
681                 result = result[0]
682             except IndexError:
683                 sys.stderr.write('file %s has no results!\n' % repr(file))
684                 return None
685         return result
686
687     def get_function_profile(self, file, function):
688         """
689         Returns the file, line number, function name, and cumulative time.
690         """
691         try:
692             import pstats
693         except ImportError, e:
694             sys.stderr.write('%s: func: %s\n' % (self.name, e))
695             sys.stderr.write('%s  This version of Python is missing the profiler.\n' % self.name_spaces)
696             sys.stderr.write('%s  Cannot use the "func" subcommand.\n' % self.name_spaces)
697             sys.exit(1)
698         statistics = pstats.Stats(file).stats
699         matches = [ e for e in statistics.items() if e[0][2] == function ]
700         r = matches[0]
701         return r[0][0], r[0][1], r[0][2], r[1][3]
702
703     def get_function_time(self, file, function):
704         """
705         Returns just the cumulative time for the specified function.
706         """
707         return self.get_function_profile(file, function)[3]
708
709     def get_memory(self, file, memory_string=None):
710         """
711         Returns a list of integers of the amount of memory used.  The
712         default behavior is to return all the stages.
713         """
714         if memory_string is None:
715             search_string = self.memory_string_all
716         else:
717             search_string = memory_string
718         lines = open(file).readlines()
719         lines = [ l for l in lines if l.startswith(search_string) ][-4:]
720         result = [ int(l.split()[-1]) for l in lines[-4:] ]
721         if len(result) == 1:
722             result = result[0]
723         return result
724
725     def get_object_counts(self, file, object_name, index=None):
726         """
727         Returns the counts of the specified object_name.
728         """
729         object_string = ' ' + object_name + '\n'
730         lines = open(file).readlines()
731         line = [ l for l in lines if l.endswith(object_string) ][0]
732         result = [ int(field) for field in line.split()[:4] ]
733         if index is not None:
734             result = result[index]
735         return result
736
737     #
738
739     command_alias = {}
740
741     def execute_subcommand(self, argv):
742         """
743         Executes the do_*() function for the specified subcommand (argv[0]).
744         """
745         if not argv:
746             return
747         cmdName = self.command_alias.get(argv[0], argv[0])
748         try:
749             func = getattr(self, 'do_' + cmdName)
750         except AttributeError:
751             return self.default(argv)
752         try:
753             return func(argv)
754         except TypeError, e:
755             sys.stderr.write("%s %s: %s\n" % (self.name, cmdName, e))
756             import traceback
757             traceback.print_exc(file=sys.stderr)
758             sys.stderr.write("Try '%s help %s'\n" % (self.name, cmdName))
759
760     def default(self, argv):
761         """
762         The default behavior for an unknown subcommand.  Prints an
763         error message and exits.
764         """
765         sys.stderr.write('%s: Unknown subcommand "%s".\n' % (self.name, argv[0]))
766         sys.stderr.write('Type "%s help" for usage.\n' % self.name)
767         sys.exit(1)
768
769     #
770
771     def do_help(self, argv):
772         """
773         """
774         if argv[1:]:
775             for arg in argv[1:]:
776                 try:
777                     func = getattr(self, 'do_' + arg)
778                 except AttributeError:
779                     sys.stderr.write('%s: No help for "%s"\n' % (self.name, arg))
780                 else:
781                     try:
782                         help = getattr(self, 'help_' + arg)
783                     except AttributeError:
784                         sys.stdout.write(self.doc_to_help(func))
785                         sys.stdout.flush()
786                     else:
787                         help()
788         else:
789             doc = self.doc_to_help(self.__class__)
790             if doc:
791                 sys.stdout.write(doc)
792             sys.stdout.flush()
793             return None
794
795     #
796
797     def help_func(self):
798         help = """\
799         Usage: scons-time func [OPTIONS] FILE [...]
800
801           -C DIR, --chdir=DIR           Change to DIR before looking for files
802           -f FILE, --file=FILE          Read configuration from specified FILE
803           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
804           --func=NAME, --function=NAME  Report time for function NAME
805           -h, --help                    Print this help and exit
806           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
807           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
808           --title=TITLE                 Specify the output plot TITLE
809         """
810         sys.stdout.write(self.outdent(help))
811         sys.stdout.flush()
812
813     def do_func(self, argv):
814         """
815         """
816         format = 'ascii'
817         function_name = '_main'
818         tail = None
819
820         short_opts = '?C:f:hp:t:'
821
822         long_opts = [
823             'chdir=',
824             'file=',
825             'fmt=',
826             'format=',
827             'func=',
828             'function=',
829             'help',
830             'prefix=',
831             'tail=',
832             'title=',
833         ]
834
835         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
836
837         for o, a in opts:
838             if o in ('-C', '--chdir'):
839                 self.chdir = a
840             elif o in ('-f', '--file'):
841                 self.config_file = a
842             elif o in ('--fmt', '--format'):
843                 format = a
844             elif o in ('--func', '--function'):
845                 function_name = a
846             elif o in ('-?', '-h', '--help'):
847                 self.do_help(['help', 'func'])
848                 sys.exit(0)
849             elif o in ('--max',):
850                 max_time = int(a)
851             elif o in ('-p', '--prefix'):
852                 self.prefix = a
853             elif o in ('-t', '--tail'):
854                 tail = int(a)
855             elif o in ('--title',):
856                 self.title = a
857
858         if self.config_file:
859             exec open(self.config_file, 'rU').read() in self.__dict__
860
861         if self.chdir:
862             os.chdir(self.chdir)
863
864         if not args:
865
866             pattern = '%s*.prof' % self.prefix
867             args = self.args_to_files([pattern], tail)
868
869             if not args:
870                 if self.chdir:
871                     directory = self.chdir
872                 else:
873                     directory = os.getcwd()
874
875                 sys.stderr.write('%s: func: No arguments specified.\n' % self.name)
876                 sys.stderr.write('%s  No %s*.prof files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
877                 sys.stderr.write('%s  Type "%s help func" for help.\n' % (self.name_spaces, self.name))
878                 sys.exit(1)
879
880         else:
881
882             args = self.args_to_files(args, tail)
883
884         cwd_ = os.getcwd() + os.sep
885
886         if format == 'ascii':
887
888             for file in args:
889                 try:
890                     f, line, func, time = \
891                             self.get_function_profile(file, function_name)
892                 except ValueError, e:
893                     sys.stderr.write("%s: func: %s: %s\n" %
894                                      (self.name, file, e))
895                 else:
896                     if f.startswith(cwd_):
897                         f = f[len(cwd_):]
898                     print "%.3f %s:%d(%s)" % (time, f, line, func)
899
900         elif format == 'gnuplot':
901
902             results = self.collect_results(args, self.get_function_time,
903                                            function_name)
904
905             self.gnuplot_results(results)
906
907         else:
908
909             sys.stderr.write('%s: func: Unknown format "%s".\n' % (self.name, format))
910             sys.exit(1)
911
912     #
913
914     def help_mem(self):
915         help = """\
916         Usage: scons-time mem [OPTIONS] FILE [...]
917
918           -C DIR, --chdir=DIR           Change to DIR before looking for files
919           -f FILE, --file=FILE          Read configuration from specified FILE
920           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
921           -h, --help                    Print this help and exit
922           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
923           --stage=STAGE                 Plot memory at the specified stage:
924                                           pre-read, post-read, pre-build,
925                                           post-build (default: post-build)
926           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
927           --title=TITLE                 Specify the output plot TITLE
928         """
929         sys.stdout.write(self.outdent(help))
930         sys.stdout.flush()
931
932     def do_mem(self, argv):
933
934         format = 'ascii'
935         logfile_path = lambda x: x
936         stage = self.default_stage
937         tail = None
938
939         short_opts = '?C:f:hp:t:'
940
941         long_opts = [
942             'chdir=',
943             'file=',
944             'fmt=',
945             'format=',
946             'help',
947             'prefix=',
948             'stage=',
949             'tail=',
950             'title=',
951         ]
952
953         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
954
955         for o, a in opts:
956             if o in ('-C', '--chdir'):
957                 self.chdir = a
958             elif o in ('-f', '--file'):
959                 self.config_file = a
960             elif o in ('--fmt', '--format'):
961                 format = a
962             elif o in ('-?', '-h', '--help'):
963                 self.do_help(['help', 'mem'])
964                 sys.exit(0)
965             elif o in ('-p', '--prefix'):
966                 self.prefix = a
967             elif o in ('--stage',):
968                 if not a in self.stages:
969                     sys.stderr.write('%s: mem: Unrecognized stage "%s".\n' % (self.name, a))
970                     sys.exit(1)
971                 stage = a
972             elif o in ('-t', '--tail'):
973                 tail = int(a)
974             elif o in ('--title',):
975                 self.title = a
976
977         if self.config_file:
978             HACK_for_exec(open(self.config_file, 'rU').read(), self.__dict__)
979
980         if self.chdir:
981             os.chdir(self.chdir)
982             logfile_path = lambda x: os.path.join(self.chdir, x)
983
984         if not args:
985
986             pattern = '%s*.log' % self.prefix
987             args = self.args_to_files([pattern], tail)
988
989             if not args:
990                 if self.chdir:
991                     directory = self.chdir
992                 else:
993                     directory = os.getcwd()
994
995                 sys.stderr.write('%s: mem: No arguments specified.\n' % self.name)
996                 sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
997                 sys.stderr.write('%s  Type "%s help mem" for help.\n' % (self.name_spaces, self.name))
998                 sys.exit(1)
999
1000         else:
1001
1002             args = self.args_to_files(args, tail)
1003
1004         cwd_ = os.getcwd() + os.sep
1005
1006         if format == 'ascii':
1007
1008             self.ascii_table(args, tuple(self.stages), self.get_memory, logfile_path)
1009
1010         elif format == 'gnuplot':
1011
1012             results = self.collect_results(args, self.get_memory,
1013                                            self.stage_strings[stage])
1014
1015             self.gnuplot_results(results)
1016
1017         else:
1018
1019             sys.stderr.write('%s: mem: Unknown format "%s".\n' % (self.name, format))
1020             sys.exit(1)
1021
1022         return 0
1023
1024     #
1025
1026     def help_obj(self):
1027         help = """\
1028         Usage: scons-time obj [OPTIONS] OBJECT FILE [...]
1029
1030           -C DIR, --chdir=DIR           Change to DIR before looking for files
1031           -f FILE, --file=FILE          Read configuration from specified FILE
1032           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
1033           -h, --help                    Print this help and exit
1034           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
1035           --stage=STAGE                 Plot memory at the specified stage:
1036                                           pre-read, post-read, pre-build,
1037                                           post-build (default: post-build)
1038           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
1039           --title=TITLE                 Specify the output plot TITLE
1040         """
1041         sys.stdout.write(self.outdent(help))
1042         sys.stdout.flush()
1043
1044     def do_obj(self, argv):
1045
1046         format = 'ascii'
1047         logfile_path = lambda x: x
1048         stage = self.default_stage
1049         tail = None
1050
1051         short_opts = '?C:f:hp:t:'
1052
1053         long_opts = [
1054             'chdir=',
1055             'file=',
1056             'fmt=',
1057             'format=',
1058             'help',
1059             'prefix=',
1060             'stage=',
1061             'tail=',
1062             'title=',
1063         ]
1064
1065         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
1066
1067         for o, a in opts:
1068             if o in ('-C', '--chdir'):
1069                 self.chdir = a
1070             elif o in ('-f', '--file'):
1071                 self.config_file = a
1072             elif o in ('--fmt', '--format'):
1073                 format = a
1074             elif o in ('-?', '-h', '--help'):
1075                 self.do_help(['help', 'obj'])
1076                 sys.exit(0)
1077             elif o in ('-p', '--prefix'):
1078                 self.prefix = a
1079             elif o in ('--stage',):
1080                 if not a in self.stages:
1081                     sys.stderr.write('%s: obj: Unrecognized stage "%s".\n' % (self.name, a))
1082                     sys.stderr.write('%s       Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
1083                     sys.exit(1)
1084                 stage = a
1085             elif o in ('-t', '--tail'):
1086                 tail = int(a)
1087             elif o in ('--title',):
1088                 self.title = a
1089
1090         if not args:
1091             sys.stderr.write('%s: obj: Must specify an object name.\n' % self.name)
1092             sys.stderr.write('%s       Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
1093             sys.exit(1)
1094
1095         object_name = args.pop(0)
1096
1097         if self.config_file:
1098             HACK_for_exec(open(self.config_file, 'rU').read(), self.__dict__)
1099
1100         if self.chdir:
1101             os.chdir(self.chdir)
1102             logfile_path = lambda x: os.path.join(self.chdir, x)
1103
1104         if not args:
1105
1106             pattern = '%s*.log' % self.prefix
1107             args = self.args_to_files([pattern], tail)
1108
1109             if not args:
1110                 if self.chdir:
1111                     directory = self.chdir
1112                 else:
1113                     directory = os.getcwd()
1114
1115                 sys.stderr.write('%s: obj: No arguments specified.\n' % self.name)
1116                 sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
1117                 sys.stderr.write('%s  Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
1118                 sys.exit(1)
1119
1120         else:
1121
1122             args = self.args_to_files(args, tail)
1123
1124         cwd_ = os.getcwd() + os.sep
1125
1126         if format == 'ascii':
1127
1128             self.ascii_table(args, tuple(self.stages), self.get_object_counts, logfile_path, object_name)
1129
1130         elif format == 'gnuplot':
1131
1132             stage_index = 0
1133             for s in self.stages:
1134                 if stage == s:
1135                     break
1136                 stage_index = stage_index + 1
1137
1138             results = self.collect_results(args, self.get_object_counts,
1139                                            object_name, stage_index)
1140
1141             self.gnuplot_results(results)
1142
1143         else:
1144
1145             sys.stderr.write('%s: obj: Unknown format "%s".\n' % (self.name, format))
1146             sys.exit(1)
1147
1148         return 0
1149
1150     #
1151
1152     def help_run(self):
1153         help = """\
1154         Usage: scons-time run [OPTIONS] [FILE ...]
1155
1156           --aegis=PROJECT               Use SCons from the Aegis PROJECT
1157           --chdir=DIR                   Name of unpacked directory for chdir
1158           -f FILE, --file=FILE          Read configuration from specified FILE
1159           -h, --help                    Print this help and exit
1160           -n, --no-exec                 No execute, just print command lines
1161           --number=NUMBER               Put output in files for run NUMBER
1162           --outdir=OUTDIR               Put output files in OUTDIR
1163           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
1164           --python=PYTHON               Time using the specified PYTHON
1165           -q, --quiet                   Don't print command lines
1166           --scons=SCONS                 Time using the specified SCONS
1167           --svn=URL, --subversion=URL   Use SCons from Subversion URL
1168           -v, --verbose                 Display output of commands
1169         """
1170         sys.stdout.write(self.outdent(help))
1171         sys.stdout.flush()
1172
1173     def do_run(self, argv):
1174         """
1175         """
1176         run_number_list = [None]
1177
1178         short_opts = '?f:hnp:qs:v'
1179
1180         long_opts = [
1181             'aegis=',
1182             'file=',
1183             'help',
1184             'no-exec',
1185             'number=',
1186             'outdir=',
1187             'prefix=',
1188             'python=',
1189             'quiet',
1190             'scons=',
1191             'svn=',
1192             'subdir=',
1193             'subversion=',
1194             'verbose',
1195         ]
1196
1197         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
1198
1199         for o, a in opts:
1200             if o in ('--aegis',):
1201                 self.aegis_project = a
1202             elif o in ('-f', '--file'):
1203                 self.config_file = a
1204             elif o in ('-?', '-h', '--help'):
1205                 self.do_help(['help', 'run'])
1206                 sys.exit(0)
1207             elif o in ('-n', '--no-exec'):
1208                 self.execute = self._do_not_execute
1209             elif o in ('--number',):
1210                 run_number_list = self.split_run_numbers(a)
1211             elif o in ('--outdir',):
1212                 self.outdir = a
1213             elif o in ('-p', '--prefix'):
1214                 self.prefix = a
1215             elif o in ('--python',):
1216                 self.python = a
1217             elif o in ('-q', '--quiet'):
1218                 self.display = self._do_not_display
1219             elif o in ('-s', '--subdir'):
1220                 self.subdir = a
1221             elif o in ('--scons',):
1222                 self.scons = a
1223             elif o in ('--svn', '--subversion'):
1224                 self.subversion_url = a
1225             elif o in ('-v', '--verbose'):
1226                 self.redirect = tee_to_file
1227                 self.verbose = True
1228                 self.svn_co_flag = ''
1229
1230         if not args and not self.config_file:
1231             sys.stderr.write('%s: run: No arguments or -f config file specified.\n' % self.name)
1232             sys.stderr.write('%s  Type "%s help run" for help.\n' % (self.name_spaces, self.name))
1233             sys.exit(1)
1234
1235         if self.config_file:
1236             exec open(self.config_file, 'rU').read() in self.__dict__
1237
1238         if args:
1239             self.archive_list = args
1240
1241         archive_file_name = os.path.split(self.archive_list[0])[1]
1242
1243         if not self.subdir:
1244             self.subdir = self.archive_splitext(archive_file_name)[0]
1245
1246         if not self.prefix:
1247             self.prefix = self.archive_splitext(archive_file_name)[0]
1248
1249         prepare = None
1250         if self.subversion_url:
1251             prepare = self.prep_subversion_run
1252         elif self.aegis_project:
1253             prepare = self.prep_aegis_run
1254
1255         for run_number in run_number_list:
1256             self.individual_run(run_number, self.archive_list, prepare)
1257
1258     def split_run_numbers(self, s):
1259         result = []
1260         for n in s.split(','):
1261             try:
1262                 x, y = n.split('-')
1263             except ValueError:
1264                 result.append(int(n))
1265             else:
1266                 result.extend(list(range(int(x), int(y)+1)))
1267         return result
1268
1269     def scons_path(self, dir):
1270         return os.path.join(dir, 'src', 'script', 'scons.py')
1271
1272     def scons_lib_dir_path(self, dir):
1273         return os.path.join(dir, 'src', 'engine')
1274
1275     def prep_aegis_run(self, commands, removals):
1276         self.aegis_tmpdir = make_temp_file(prefix = self.name + '-aegis-')
1277         removals.append((shutil.rmtree, 'rm -rf %%s', self.aegis_tmpdir))
1278
1279         self.aegis_parent_project = os.path.splitext(self.aegis_project)[0]
1280         self.scons = self.scons_path(self.aegis_tmpdir)
1281         self.scons_lib_dir = self.scons_lib_dir_path(self.aegis_tmpdir)
1282
1283         commands.extend([
1284             'mkdir %(aegis_tmpdir)s',
1285             (lambda: os.chdir(self.aegis_tmpdir), 'cd %(aegis_tmpdir)s'),
1286             '%(aegis)s -cp -ind -p %(aegis_parent_project)s .',
1287             '%(aegis)s -cp -ind -p %(aegis_project)s -delta %(run_number)s .',
1288         ])
1289
1290     def prep_subversion_run(self, commands, removals):
1291         self.svn_tmpdir = make_temp_file(prefix = self.name + '-svn-')
1292         removals.append((shutil.rmtree, 'rm -rf %%s', self.svn_tmpdir))
1293
1294         self.scons = self.scons_path(self.svn_tmpdir)
1295         self.scons_lib_dir = self.scons_lib_dir_path(self.svn_tmpdir)
1296
1297         commands.extend([
1298             'mkdir %(svn_tmpdir)s',
1299             '%(svn)s co %(svn_co_flag)s -r %(run_number)s %(subversion_url)s %(svn_tmpdir)s',
1300         ])
1301
1302     def individual_run(self, run_number, archive_list, prepare=None):
1303         """
1304         Performs an individual run of the default SCons invocations.
1305         """
1306
1307         commands = []
1308         removals = []
1309
1310         if prepare:
1311             prepare(commands, removals)
1312
1313         save_scons              = self.scons
1314         save_scons_wrapper      = self.scons_wrapper
1315         save_scons_lib_dir      = self.scons_lib_dir
1316
1317         if self.outdir is None:
1318             self.outdir = self.orig_cwd
1319         elif not os.path.isabs(self.outdir):
1320             self.outdir = os.path.join(self.orig_cwd, self.outdir)
1321
1322         if self.scons is None:
1323             self.scons = self.scons_path(self.orig_cwd)
1324
1325         if self.scons_lib_dir is None:
1326             self.scons_lib_dir = self.scons_lib_dir_path(self.orig_cwd)
1327
1328         if self.scons_wrapper is None:
1329             self.scons_wrapper = self.scons
1330
1331         if not run_number:
1332             run_number = self.find_next_run_number(self.outdir, self.prefix)
1333
1334         self.run_number = str(run_number)
1335
1336         self.prefix_run = self.prefix + '-%03d' % run_number
1337
1338         if self.targets0 is None:
1339             self.targets0 = self.startup_targets
1340         if self.targets1 is None:
1341             self.targets1 = self.targets
1342         if self.targets2 is None:
1343             self.targets2 = self.targets
1344
1345         self.tmpdir = make_temp_file(prefix = self.name + '-')
1346
1347         commands.extend([
1348             'mkdir %(tmpdir)s',
1349
1350             (os.chdir, 'cd %%s', self.tmpdir),
1351         ])
1352
1353         for archive in archive_list:
1354             if not os.path.isabs(archive):
1355                 archive = os.path.join(self.orig_cwd, archive)
1356             if os.path.isdir(archive):
1357                 dest = os.path.split(archive)[1]
1358                 commands.append((shutil.copytree, 'cp -r %%s %%s', archive, dest))
1359             else:
1360                 suffix = self.archive_splitext(archive)[1]
1361                 unpack_command = self.unpack_map.get(suffix)
1362                 if not unpack_command:
1363                     dest = os.path.split(archive)[1]
1364                     commands.append((shutil.copyfile, 'cp %%s %%s', archive, dest))
1365                 else:
1366                     commands.append(unpack_command + (archive,))
1367
1368         commands.extend([
1369             (os.chdir, 'cd %%s', self.subdir),
1370         ])
1371
1372         commands.extend(self.initial_commands)
1373
1374         commands.extend([
1375             (lambda: read_tree('.'),
1376             'find * -type f | xargs cat > /dev/null'),
1377
1378             (self.set_env, 'export %%s=%%s',
1379              'SCONS_LIB_DIR', self.scons_lib_dir),
1380
1381             '%(python)s %(scons_wrapper)s --version',
1382         ])
1383
1384         index = 0
1385         for run_command in self.run_commands:
1386             setattr(self, 'prof%d' % index, self.profile_name(index))
1387             c = (
1388                 self.log_execute,
1389                 self.log_display,
1390                 run_command,
1391                 self.logfile_name(index),
1392             )
1393             commands.append(c)
1394             index = index + 1
1395
1396         commands.extend([
1397             (os.chdir, 'cd %%s', self.orig_cwd),
1398         ])
1399
1400         if not os.environ.get('PRESERVE'):
1401             commands.extend(removals)
1402
1403             commands.append((shutil.rmtree, 'rm -rf %%s', self.tmpdir))
1404
1405         self.run_command_list(commands, self.__dict__)
1406
1407         self.scons              = save_scons
1408         self.scons_lib_dir      = save_scons_lib_dir
1409         self.scons_wrapper      = save_scons_wrapper
1410
1411     #
1412
1413     def help_time(self):
1414         help = """\
1415         Usage: scons-time time [OPTIONS] FILE [...]
1416
1417           -C DIR, --chdir=DIR           Change to DIR before looking for files
1418           -f FILE, --file=FILE          Read configuration from specified FILE
1419           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
1420           -h, --help                    Print this help and exit
1421           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
1422           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
1423           --which=TIMER                 Plot timings for TIMER:  total,
1424                                           SConscripts, SCons, commands.
1425         """
1426         sys.stdout.write(self.outdent(help))
1427         sys.stdout.flush()
1428
1429     def do_time(self, argv):
1430
1431         format = 'ascii'
1432         logfile_path = lambda x: x
1433         tail = None
1434         which = 'total'
1435
1436         short_opts = '?C:f:hp:t:'
1437
1438         long_opts = [
1439             'chdir=',
1440             'file=',
1441             'fmt=',
1442             'format=',
1443             'help',
1444             'prefix=',
1445             'tail=',
1446             'title=',
1447             'which=',
1448         ]
1449
1450         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
1451
1452         for o, a in opts:
1453             if o in ('-C', '--chdir'):
1454                 self.chdir = a
1455             elif o in ('-f', '--file'):
1456                 self.config_file = a
1457             elif o in ('--fmt', '--format'):
1458                 format = a
1459             elif o in ('-?', '-h', '--help'):
1460                 self.do_help(['help', 'time'])
1461                 sys.exit(0)
1462             elif o in ('-p', '--prefix'):
1463                 self.prefix = a
1464             elif o in ('-t', '--tail'):
1465                 tail = int(a)
1466             elif o in ('--title',):
1467                 self.title = a
1468             elif o in ('--which',):
1469                 if not a in self.time_strings.keys():
1470                     sys.stderr.write('%s: time: Unrecognized timer "%s".\n' % (self.name, a))
1471                     sys.stderr.write('%s  Type "%s help time" for help.\n' % (self.name_spaces, self.name))
1472                     sys.exit(1)
1473                 which = a
1474
1475         if self.config_file:
1476             HACK_for_exec(open(self.config_file, 'rU').read(), self.__dict__)
1477
1478         if self.chdir:
1479             os.chdir(self.chdir)
1480             logfile_path = lambda x: os.path.join(self.chdir, x)
1481
1482         if not args:
1483
1484             pattern = '%s*.log' % self.prefix
1485             args = self.args_to_files([pattern], tail)
1486
1487             if not args:
1488                 if self.chdir:
1489                     directory = self.chdir
1490                 else:
1491                     directory = os.getcwd()
1492
1493                 sys.stderr.write('%s: time: No arguments specified.\n' % self.name)
1494                 sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
1495                 sys.stderr.write('%s  Type "%s help time" for help.\n' % (self.name_spaces, self.name))
1496                 sys.exit(1)
1497
1498         else:
1499
1500             args = self.args_to_files(args, tail)
1501
1502         cwd_ = os.getcwd() + os.sep
1503
1504         if format == 'ascii':
1505
1506             columns = ("Total", "SConscripts", "SCons", "commands")
1507             self.ascii_table(args, columns, self.get_debug_times, logfile_path)
1508
1509         elif format == 'gnuplot':
1510
1511             results = self.collect_results(args, self.get_debug_times,
1512                                            self.time_strings[which])
1513
1514             self.gnuplot_results(results, fmt='%s %.6f')
1515
1516         else:
1517
1518             sys.stderr.write('%s: time: Unknown format "%s".\n' % (self.name, format))
1519             sys.exit(1)
1520
1521 if __name__ == '__main__':
1522     opts, args = getopt.getopt(sys.argv[1:], 'h?V', ['help', 'version'])
1523
1524     ST = SConsTimer()
1525
1526     for o, a in opts:
1527         if o in ('-?', '-h', '--help'):
1528             ST.do_help(['help'])
1529             sys.exit(0)
1530         elif o in ('-V', '--version'):
1531             sys.stdout.write('scons-time version\n')
1532             sys.exit(0)
1533
1534     if not args:
1535         sys.stderr.write('Type "%s help" for usage.\n' % ST.name)
1536         sys.exit(1)
1537
1538     ST.execute_subcommand(args)
1539
1540 # Local Variables:
1541 # tab-width:4
1542 # indent-tabs-mode:nil
1543 # End:
1544 # vim: set expandtab tabstop=4 shiftwidth=4: