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