Merged revisions 1968-2115 via svnmerge from
[scons.git] / src / engine / SCons / Script / Main.py
1 """SCons.Script
2
3 This file implements the main() function used by the scons script.
4
5 Architecturally, this *is* the scons script, and will likely only be
6 called from the external "scons" wrapper.  Consequently, anything here
7 should not be, or be considered, part of the build engine.  If it's
8 something that we expect other software to want to use, it should go in
9 some other module.  If it's specific to the "scons" script invocation,
10 it goes here.
11
12 """
13
14 #
15 # __COPYRIGHT__
16 #
17 # Permission is hereby granted, free of charge, to any person obtaining
18 # a copy of this software and associated documentation files (the
19 # "Software"), to deal in the Software without restriction, including
20 # without limitation the rights to use, copy, modify, merge, publish,
21 # distribute, sublicense, and/or sell copies of the Software, and to
22 # permit persons to whom the Software is furnished to do so, subject to
23 # the following conditions:
24 #
25 # The above copyright notice and this permission notice shall be included
26 # in all copies or substantial portions of the Software.
27 #
28 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
29 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
30 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
32 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
33 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 #
36
37 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
38
39 import SCons.compat
40
41 import os
42 import os.path
43 import string
44 import sys
45 import time
46 import traceback
47
48 # Strip the script directory from sys.path() so on case-insensitive
49 # (Windows) systems Python doesn't think that the "scons" script is the
50 # "SCons" package.  Replace it with our own version directory so, if
51 # if they're there, we pick up the right version of the build engine
52 # modules.
53 #sys.path = [os.path.join(sys.prefix,
54 #                         'lib',
55 #                         'scons-%d' % SCons.__version__)] + sys.path[1:]
56
57 import SCons.Debug
58 import SCons.Defaults
59 import SCons.Environment
60 import SCons.Errors
61 import SCons.Job
62 import SCons.Node
63 import SCons.Node.FS
64 import SCons.SConf
65 import SCons.Script
66 import SCons.Sig
67 import SCons.Taskmaster
68 import SCons.Util
69 import SCons.Warnings
70
71 #
72
73 class SConsPrintHelpException(Exception):
74     pass
75
76 display = SCons.Util.display
77 progress_display = SCons.Util.DisplayEngine()
78
79 first_command_start = None
80 last_command_end = None
81
82 # Task control.
83 #
84 class BuildTask(SCons.Taskmaster.Task):
85     """An SCons build task."""
86     def display(self, message):
87         display('scons: ' + message)
88
89     def execute(self):
90         for target in self.targets:
91             if target.get_state() == SCons.Node.up_to_date: 
92                 continue
93             if target.has_builder() and not hasattr(target.builder, 'status'):
94                 if print_time:
95                     start_time = time.time()
96                     global first_command_start
97                     if first_command_start is None:
98                         first_command_start = start_time
99                 SCons.Taskmaster.Task.execute(self)
100                 if print_time:
101                     global cumulative_command_time
102                     global last_command_end
103                     finish_time = time.time()
104                     last_command_end = finish_time
105                     cumulative_command_time = cumulative_command_time+finish_time-start_time
106                     sys.stdout.write("Command execution time: %f seconds\n"%(finish_time-start_time))
107                 break
108         else:
109             if self.top and target.has_builder():
110                 display("scons: `%s' is up to date." % str(self.node))
111
112     def do_failed(self, status=2):
113         global exit_status
114         if self.options.ignore_errors:
115             SCons.Taskmaster.Task.executed(self)
116         elif self.options.keep_going:
117             SCons.Taskmaster.Task.fail_continue(self)
118             exit_status = status
119         else:
120             SCons.Taskmaster.Task.fail_stop(self)
121             exit_status = status
122             
123     def executed(self):
124         t = self.targets[0]
125         if self.top and not t.has_builder() and not t.side_effect:
126             if not t.exists():
127                 sys.stderr.write("scons: *** Do not know how to make target `%s'." % t)
128                 if not self.options.keep_going:
129                     sys.stderr.write("  Stop.")
130                 sys.stderr.write("\n")
131                 self.do_failed()
132             else:
133                 print "scons: Nothing to be done for `%s'." % t
134                 SCons.Taskmaster.Task.executed(self)
135         else:
136             SCons.Taskmaster.Task.executed(self)
137
138     def failed(self):
139         # Handle the failure of a build task.  The primary purpose here
140         # is to display the various types of Errors and Exceptions
141         # appropriately.
142         status = 2
143         exc_info = self.exc_info()
144         try:
145             t, e, tb = exc_info
146         except ValueError:
147             t, e = exc_info
148             tb = None
149         if t is None:
150             # The Taskmaster didn't record an exception for this Task;
151             # see if the sys module has one.
152             t, e = sys.exc_info()[:2]
153
154         def nodestring(n):
155             if not SCons.Util.is_List(n):
156                 n = [ n ]
157             return string.join(map(str, n), ', ')
158
159         errfmt = "scons: *** [%s] %s\n"
160
161         if t == SCons.Errors.BuildError:
162             tname = nodestring(e.node)
163             errstr = e.errstr
164             if e.filename:
165                 errstr = e.filename + ': ' + errstr
166             sys.stderr.write(errfmt % (tname, errstr))
167         elif t == SCons.Errors.TaskmasterException:
168             tname = nodestring(e.node)
169             sys.stderr.write(errfmt % (tname, e.errstr))
170             type, value, trace = e.exc_info
171             traceback.print_exception(type, value, trace)
172         elif t == SCons.Errors.ExplicitExit:
173             status = e.status
174             tname = nodestring(e.node)
175             errstr = 'Explicit exit, status %s' % status
176             sys.stderr.write(errfmt % (tname, errstr))
177         else:
178             if e is None:
179                 e = t
180             s = str(e)
181             if t == SCons.Errors.StopError and not self.options.keep_going:
182                 s = s + '  Stop.'
183             sys.stderr.write("scons: *** %s\n" % s)
184
185             if tb and print_stacktrace:
186                 sys.stderr.write("scons: internal stack trace:\n")
187                 traceback.print_tb(tb, file=sys.stderr)
188
189         self.do_failed(status)
190
191         self.exc_clear()
192
193     def postprocess(self):
194         if self.top:
195             t = self.targets[0]
196             for tp in self.options.tree_printers:
197                 tp.display(t)
198             if self.options.debug_includes:
199                 tree = t.render_include_tree()
200                 if tree:
201                     print
202                     print tree
203         SCons.Taskmaster.Task.postprocess(self)
204
205     def make_ready(self):
206         """Make a task ready for execution"""
207         SCons.Taskmaster.Task.make_ready(self)
208         if self.out_of_date and self.options.debug_explain:
209             explanation = self.out_of_date[0].explain()
210             if explanation:
211                 sys.stdout.write("scons: " + explanation)
212
213 class CleanTask(SCons.Taskmaster.Task):
214     """An SCons clean task."""
215     def dir_index(self, directory):
216         dirname = lambda f, d=directory: os.path.join(d, f)
217         files = map(dirname, os.listdir(directory))
218
219         # os.listdir() isn't guaranteed to return files in any specific order,
220         # but some of the test code expects sorted output.
221         files.sort()
222         return files
223
224     def fs_delete(self, path, remove=1):
225         try:
226             if os.path.exists(path):
227                 if os.path.isfile(path):
228                     if remove: os.unlink(path)
229                     display("Removed " + path)
230                 elif os.path.isdir(path) and not os.path.islink(path):
231                     # delete everything in the dir
232                     for p in self.dir_index(path):
233                         if os.path.isfile(p):
234                             if remove: os.unlink(p)
235                             display("Removed " + p)
236                         else:
237                             self.fs_delete(p, remove)
238                     # then delete dir itself
239                     if remove: os.rmdir(path)
240                     display("Removed directory " + path)
241         except (IOError, OSError), e:
242             print "scons: Could not remove '%s':" % str(path), e.strerror
243
244     def show(self):
245         target = self.targets[0]
246         if (target.has_builder() or target.side_effect) and not target.noclean:
247             for t in self.targets:
248                 if not t.isdir():
249                     display("Removed " + str(t))
250         if SCons.Environment.CleanTargets.has_key(target):
251             files = SCons.Environment.CleanTargets[target]
252             for f in files:
253                 self.fs_delete(str(f), 0)
254
255     def remove(self):
256         target = self.targets[0]
257         if (target.has_builder() or target.side_effect) and not target.noclean:
258             for t in self.targets:
259                 try:
260                     removed = t.remove()
261                 except OSError, e:
262                     # An OSError may indicate something like a permissions
263                     # issue, an IOError would indicate something like
264                     # the file not existing.  In either case, print a
265                     # message and keep going to try to remove as many
266                     # targets aa possible.
267                     print "scons: Could not remove '%s':" % str(t), e.strerror
268                 else:
269                     if removed:
270                         display("Removed " + str(t))
271         if SCons.Environment.CleanTargets.has_key(target):
272             files = SCons.Environment.CleanTargets[target]
273             for f in files:
274                 self.fs_delete(str(f))
275
276     execute = remove
277
278     # Have the taskmaster arrange to "execute" all of the targets, because
279     # we'll figure out ourselves (in remove() or show() above) whether
280     # anything really needs to be done.
281     make_ready = SCons.Taskmaster.Task.make_ready_all
282
283     def prepare(self):
284         pass
285
286 class QuestionTask(SCons.Taskmaster.Task):
287     """An SCons task for the -q (question) option."""
288     def prepare(self):
289         pass
290     
291     def execute(self):
292         if self.targets[0].get_state() != SCons.Node.up_to_date:
293             global exit_status
294             exit_status = 1
295             self.tm.stop()
296
297     def executed(self):
298         pass
299
300
301 class TreePrinter:
302     def __init__(self, derived=False, prune=False, status=False):
303         self.derived = derived
304         self.prune = prune
305         self.status = status
306     def get_all_children(self, node):
307         return node.all_children()
308     def get_derived_children(self, node):
309         children = node.all_children(None)
310         return filter(lambda x: x.has_builder(), children)
311     def display(self, t):
312         if self.derived:
313             func = self.get_derived_children
314         else:
315             func = self.get_all_children
316         s = self.status and 2 or 0
317         SCons.Util.print_tree(t, func, prune=self.prune, showtags=s)
318
319
320 # Global variables
321
322 print_objects = 0
323 print_memoizer = 0
324 print_stacktrace = 0
325 print_time = 0
326 sconscript_time = 0
327 cumulative_command_time = 0
328 exit_status = 0 # exit status, assume success by default
329 num_jobs = None
330 delayed_warnings = []
331
332 OptionsParser = None
333
334 def AddOption(*args, **kw):
335     if not kw.has_key('default'):
336         kw['default'] = None
337     result = apply(OptionsParser.add_local_option, args, kw)
338     return result
339 #
340 class Stats:
341     def __init__(self):
342         self.stats = []
343         self.labels = []
344         self.append = self.do_nothing
345         self.print_stats = self.do_nothing
346     def enable(self, outfp):
347         self.outfp = outfp
348         self.append = self.do_append
349         self.print_stats = self.do_print
350     def do_nothing(self, *args, **kw):
351         pass
352
353 class CountStats(Stats):
354     def do_append(self, label):
355         self.labels.append(label)
356         self.stats.append(SCons.Debug.fetchLoggedInstances())
357     def do_print(self):
358         stats_table = {}
359         for s in self.stats:
360             for n in map(lambda t: t[0], s):
361                 stats_table[n] = [0, 0, 0, 0]
362         i = 0
363         for s in self.stats:
364             for n, c in s:
365                 stats_table[n][i] = c
366             i = i + 1
367         keys = stats_table.keys()
368         keys.sort()
369         self.outfp.write("Object counts:\n")
370         pre = ["   "]
371         post = ["   %s\n"]
372         l = len(self.stats)
373         fmt1 = string.join(pre + [' %7s']*l + post, '')
374         fmt2 = string.join(pre + [' %7d']*l + post, '')
375         labels = self.labels[:l]
376         labels.append(("", "Class"))
377         self.outfp.write(fmt1 % tuple(map(lambda x: x[0], labels)))
378         self.outfp.write(fmt1 % tuple(map(lambda x: x[1], labels)))
379         for k in keys:
380             r = stats_table[k][:l] + [k]
381             self.outfp.write(fmt2 % tuple(r))
382
383 count_stats = CountStats()
384
385 class MemStats(Stats):
386     def do_append(self, label):
387         self.labels.append(label)
388         self.stats.append(SCons.Debug.memory())
389     def do_print(self):
390         fmt = 'Memory %-32s %12d\n'
391         for label, stats in map(None, self.labels, self.stats):
392             self.outfp.write(fmt % (label, stats))
393
394 memory_stats = MemStats()
395
396 # utility functions
397
398 def _scons_syntax_error(e):
399     """Handle syntax errors. Print out a message and show where the error
400     occurred.
401     """
402     etype, value, tb = sys.exc_info()
403     lines = traceback.format_exception_only(etype, value)
404     for line in lines:
405         sys.stderr.write(line+'\n')
406     sys.exit(2)
407
408 def find_deepest_user_frame(tb):
409     """
410     Find the deepest stack frame that is not part of SCons.
411
412     Input is a "pre-processed" stack trace in the form
413     returned by traceback.extract_tb() or traceback.extract_stack()
414     """
415     
416     tb.reverse()
417
418     # find the deepest traceback frame that is not part
419     # of SCons:
420     for frame in tb:
421         filename = frame[0]
422         if string.find(filename, os.sep+'SCons'+os.sep) == -1:
423             return frame
424     return tb[0]
425
426 def _scons_user_error(e):
427     """Handle user errors. Print out a message and a description of the
428     error, along with the line number and routine where it occured. 
429     The file and line number will be the deepest stack frame that is
430     not part of SCons itself.
431     """
432     global print_stacktrace
433     etype, value, tb = sys.exc_info()
434     if print_stacktrace:
435         traceback.print_exception(etype, value, tb)
436     filename, lineno, routine, dummy = find_deepest_user_frame(traceback.extract_tb(tb))
437     sys.stderr.write("\nscons: *** %s\n" % value)
438     sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
439     sys.exit(2)
440
441 def _scons_user_warning(e):
442     """Handle user warnings. Print out a message and a description of
443     the warning, along with the line number and routine where it occured.
444     The file and line number will be the deepest stack frame that is
445     not part of SCons itself.
446     """
447     etype, value, tb = sys.exc_info()
448     filename, lineno, routine, dummy = find_deepest_user_frame(traceback.extract_tb(tb))
449     sys.stderr.write("\nscons: warning: %s\n" % e)
450     sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
451
452 def _scons_internal_warning(e):
453     """Slightly different from _scons_user_warning in that we use the
454     *current call stack* rather than sys.exc_info() to get our stack trace.
455     This is used by the warnings framework to print warnings."""
456     filename, lineno, routine, dummy = find_deepest_user_frame(traceback.extract_stack())
457     sys.stderr.write("\nscons: warning: %s\n" % e[0])
458     sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
459
460 def _scons_internal_error():
461     """Handle all errors but user errors. Print out a message telling
462     the user what to do in this case and print a normal trace.
463     """
464     print 'internal error'
465     traceback.print_exc()
466     sys.exit(2)
467
468 def _setup_warn(arg):
469     """The --warn option.  An argument to this option
470     should be of the form <warning-class> or no-<warning-class>.
471     The warning class is munged in order to get an actual class
472     name from the SCons.Warnings module to enable or disable.
473     The supplied <warning-class> is split on hyphens, each element
474     is captialized, then smushed back together.  Then the string
475     "SCons.Warnings." is added to the front and "Warning" is added
476     to the back to get the fully qualified class name.
477
478     For example, --warn=deprecated will enable the
479     SCons.Warnings.DeprecatedWarning class.
480
481     --warn=no-dependency will disable the
482     SCons.Warnings.DependencyWarning class.
483
484     As a special case, --warn=all and --warn=no-all
485     will enable or disable (respectively) the base
486     class of all warnings, which is SCons.Warning.Warning."""
487
488     elems = string.split(string.lower(arg), '-')
489     enable = 1
490     if elems[0] == 'no':
491         enable = 0
492         del elems[0]
493
494     if len(elems) == 1 and elems[0] == 'all':
495         class_name = "Warning"
496     else:
497         def _capitalize(s):
498             if s[:5] == "scons":
499                 return "SCons" + s[5:]
500             else:
501                 return string.capitalize(s)
502         class_name = string.join(map(_capitalize, elems), '') + "Warning"
503     try:
504         clazz = getattr(SCons.Warnings, class_name)
505     except AttributeError:
506         sys.stderr.write("No warning type: '%s'\n" % arg)
507     else:
508         if enable:
509             SCons.Warnings.enableWarningClass(clazz)
510         else:
511             SCons.Warnings.suppressWarningClass(clazz)
512
513 def _SConstruct_exists(dirname='', repositories=[]):
514     """This function checks that an SConstruct file exists in a directory.
515     If so, it returns the path of the file. By default, it checks the
516     current directory.
517     """
518     for file in ['SConstruct', 'Sconstruct', 'sconstruct']:
519         sfile = os.path.join(dirname, file)
520         if os.path.isfile(sfile):
521             return sfile
522         if not os.path.isabs(sfile):
523             for rep in repositories:
524                 if os.path.isfile(os.path.join(rep, sfile)):
525                     return sfile
526     return None
527
528 def _set_debug_values(options):
529     global print_memoizer, print_objects, print_stacktrace, print_time
530
531     debug_values = options.debug
532
533     if "count" in debug_values:
534         # All of the object counts are within "if __debug__:" blocks,
535         # which get stripped when running optimized (with python -O or
536         # from compiled *.pyo files).  Provide a warning if __debug__ is
537         # stripped, so it doesn't just look like --debug=count is broken.
538         enable_count = False
539         if __debug__: enable_count = True
540         if enable_count:
541             count_stats.enable(sys.stdout)
542         else:
543             msg = "--debug=count is not supported when running SCons\n" + \
544                   "\twith the python -O option or optimized (.pyo) modules."
545             SCons.Warnings.warn(SCons.Warnings.NoObjectCountWarning, msg)
546     if "dtree" in debug_values:
547         options.tree_printers.append(TreePrinter(derived=True))
548     options.debug_explain = ("explain" in debug_values)
549     if "findlibs" in debug_values:
550         SCons.Scanner.Prog.print_find_libs = "findlibs"
551     options.debug_includes = ("includes" in debug_values)
552     print_memoizer = ("memoizer" in debug_values)
553     if "memory" in debug_values:
554         memory_stats.enable(sys.stdout)
555     print_objects = ("objects" in debug_values)
556     if "presub" in debug_values:
557         SCons.Action.print_actions_presub = 1
558     if "stacktrace" in debug_values:
559         print_stacktrace = 1
560     if "stree" in debug_values:
561         options.tree_printers.append(TreePrinter(status=True))
562     if "time" in debug_values:
563         print_time = 1
564     if "tree" in debug_values:
565         options.tree_printers.append(TreePrinter())
566
567 def _create_path(plist):
568     path = '.'
569     for d in plist:
570         if os.path.isabs(d):
571             path = d
572         else:
573             path = path + '/' + d
574     return path
575
576 def _load_site_scons_dir(topdir, site_dir_name=None):
577     """Load the site_scons dir under topdir.
578     Adds site_scons to sys.path, imports site_scons/site_init.py,
579     and adds site_scons/site_tools to default toolpath."""
580     if site_dir_name:
581         err_if_not_found = True       # user specified: err if missing
582     else:
583         site_dir_name = "site_scons"
584         err_if_not_found = False
585         
586     site_dir = os.path.join(topdir.path, site_dir_name)
587     if not os.path.exists(site_dir):
588         if err_if_not_found:
589             raise SCons.Errors.UserError, "site dir %s not found."%site_dir
590         return
591
592     site_init_filename = "site_init.py"
593     site_init_modname = "site_init"
594     site_tools_dirname = "site_tools"
595     sys.path = [os.path.abspath(site_dir)] + sys.path
596     site_init_file = os.path.join(site_dir, site_init_filename)
597     site_tools_dir = os.path.join(site_dir, site_tools_dirname)
598     if os.path.exists(site_init_file):
599         import imp
600         try:
601             fp, pathname, description = imp.find_module(site_init_modname,
602                                                         [site_dir])
603             try:
604                 imp.load_module(site_init_modname, fp, pathname, description)
605             finally:
606                 if fp:
607                     fp.close()
608         except ImportError, e:
609             sys.stderr.write("Can't import site init file '%s': %s\n"%(site_init_file, e))
610             raise
611         except Exception, e:
612             sys.stderr.write("Site init file '%s' raised exception: %s\n"%(site_init_file, e))
613             raise
614     if os.path.exists(site_tools_dir):
615         SCons.Tool.DefaultToolpath.append(os.path.abspath(site_tools_dir))
616
617 def version_string(label, module):
618     fmt = "\t%s: v%s.%s, %s, by %s on %s\n"
619     return fmt % (label,
620                   module.__version__,
621                   module.__build__,
622                   module.__date__,
623                   module.__developer__,
624                   module.__buildsys__)
625
626 def _main(parser):
627     global exit_status
628
629     options = parser.values
630
631     # Here's where everything really happens.
632
633     # First order of business:  set up default warnings and then
634     # handle the user's warning options, so that we can issue (or
635     # suppress) appropriate warnings about anything that might happen,
636     # as configured by the user.
637
638     default_warnings = [ SCons.Warnings.CorruptSConsignWarning,
639                          SCons.Warnings.DeprecatedWarning,
640                          SCons.Warnings.DuplicateEnvironmentWarning,
641                          SCons.Warnings.MissingSConscriptWarning,
642                          SCons.Warnings.NoMD5ModuleWarning,
643                          SCons.Warnings.NoMetaclassSupportWarning,
644                          SCons.Warnings.NoObjectCountWarning,
645                          SCons.Warnings.NoParallelSupportWarning,
646                          SCons.Warnings.MisleadingKeywordsWarning, ]
647     for warning in default_warnings:
648         SCons.Warnings.enableWarningClass(warning)
649     SCons.Warnings._warningOut = _scons_internal_warning
650     if options.warn:
651         _setup_warn(options.warn)
652
653     # Now that we have the warnings configuration set up, we can actually
654     # issue (or suppress) any warnings about warning-worthy things that
655     # occurred while the command-line options were getting parsed.
656     try:
657         dw = options.delayed_warnings
658     except AttributeError:
659         pass
660     else:
661         delayed_warnings.extend(dw)
662     for warning_type, message in delayed_warnings:
663         SCons.Warnings.warn(warning_type, message)
664
665     if options.diskcheck:
666         SCons.Node.FS.set_diskcheck(options.diskcheck)
667
668     # Next, we want to create the FS object that represents the outside
669     # world's file system, as that's central to a lot of initialization.
670     # To do this, however, we need to be in the directory from which we
671     # want to start everything, which means first handling any relevant
672     # options that might cause us to chdir somewhere (-C, -D, -U, -u).
673     if options.directory:
674         cdir = _create_path(options.directory)
675         try:
676             os.chdir(cdir)
677         except OSError:
678             sys.stderr.write("Could not change directory to %s\n" % cdir)
679
680     target_top = None
681     if options.climb_up:
682         target_top = '.'  # directory to prepend to targets
683         script_dir = os.getcwd()  # location of script
684         while script_dir and not _SConstruct_exists(script_dir, options.repository):
685             script_dir, last_part = os.path.split(script_dir)
686             if last_part:
687                 target_top = os.path.join(last_part, target_top)
688             else:
689                 script_dir = ''
690         if script_dir:
691             display("scons: Entering directory `%s'" % script_dir)
692             os.chdir(script_dir)
693
694     # Now that we're in the top-level SConstruct directory, go ahead
695     # and initialize the FS object that represents the file system,
696     # and make it the build engine default.
697     fs = SCons.Node.FS.default_fs = SCons.Node.FS.FS()
698
699     for rep in options.repository:
700         fs.Repository(rep)
701
702     # Now that we have the FS object, the next order of business is to
703     # check for an SConstruct file (or other specified config file).
704     # If there isn't one, we can bail before doing any more work.
705     scripts = []
706     if options.file:
707         scripts.extend(options.file)
708     if not scripts:
709         sfile = _SConstruct_exists(repositories=options.repository)
710         if sfile:
711             scripts.append(sfile)
712
713     if not scripts:
714         if options.help:
715             # There's no SConstruct, but they specified -h.
716             # Give them the options usage now, before we fail
717             # trying to read a non-existent SConstruct file.
718             raise SConsPrintHelpException
719         raise SCons.Errors.UserError, "No SConstruct file found."
720
721     if scripts[0] == "-":
722         d = fs.getcwd()
723     else:
724         d = fs.File(scripts[0]).dir
725     fs.set_SConstruct_dir(d)
726
727     _set_debug_values(options)
728     SCons.Node.implicit_cache = options.implicit_cache
729     SCons.Node.implicit_deps_changed = options.implicit_deps_changed
730     SCons.Node.implicit_deps_unchanged = options.implicit_deps_unchanged
731     if options.no_exec:
732         SCons.SConf.dryrun = 1
733         SCons.Action.execute_actions = None
734         CleanTask.execute = CleanTask.show
735     if options.question:
736         SCons.SConf.dryrun = 1
737     SCons.SConf.SetCacheMode(options.config)
738     SCons.SConf.SetProgressDisplay(progress_display)
739
740     if options.no_progress or options.silent:
741         progress_display.set_mode(0)
742     if options.silent:
743         display.set_mode(0)
744     if options.silent:
745         SCons.Action.print_actions = None
746
747     if options.cache_debug:
748         fs.CacheDebugEnable(options.cache_debug)
749     if options.cache_disable:
750         def disable(self): pass
751         fs.CacheDir = disable
752     if options.cache_force:
753         fs.cache_force = 1
754     if options.cache_show:
755         fs.cache_show = 1
756
757     if options.site_dir:
758         _load_site_scons_dir(d, options.site_dir)
759     elif not options.no_site_dir:
760         _load_site_scons_dir(d)
761         
762     if options.include_dir:
763         sys.path = options.include_dir + sys.path
764
765     # That should cover (most of) the options.  Next, set up the variables
766     # that hold command-line arguments, so the SConscript files that we
767     # read and execute have access to them.
768     targets = []
769     xmit_args = []
770     for a in parser.largs:
771         if a[0] == '-':
772             continue
773         if '=' in a:
774             xmit_args.append(a)
775         else:
776             targets.append(a)
777     SCons.Script._Add_Targets(targets + parser.rargs)
778     SCons.Script._Add_Arguments(xmit_args)
779
780     sys.stdout = SCons.Util.Unbuffered(sys.stdout)
781
782     memory_stats.append('before reading SConscript files:')
783     count_stats.append(('pre-', 'read'))
784
785     # And here's where we (finally) read the SConscript files.
786
787     progress_display("scons: Reading SConscript files ...")
788
789     start_time = time.time()
790     try:
791         for script in scripts:
792             SCons.Script._SConscript._SConscript(fs, script)
793     except SCons.Errors.StopError, e:
794         # We had problems reading an SConscript file, such as it
795         # couldn't be copied in to the BuildDir.  Since we're just
796         # reading SConscript files and haven't started building
797         # things yet, stop regardless of whether they used -i or -k
798         # or anything else.
799         sys.stderr.write("scons: *** %s  Stop.\n" % e)
800         exit_status = 2
801         sys.exit(exit_status)
802     global sconscript_time
803     sconscript_time = time.time() - start_time
804
805     progress_display("scons: done reading SConscript files.")
806
807     memory_stats.append('after reading SConscript files:')
808     count_stats.append(('post-', 'read'))
809
810     SCons.SConf.CreateConfigHBuilder(SCons.Defaults.DefaultEnvironment())
811
812     # Now re-parse the command-line options (any to the left of a '--'
813     # argument, that is) with any user-defined command-line options that
814     # the SConscript files may have added to the parser object.  This will
815     # emit the appropriate error message and exit if any unknown option
816     # was specified on the command line.
817
818     parser.preserve_unknown_options = False
819     parser.parse_args(parser.largs, options)
820
821     if options.help:
822         help_text = SCons.Script.help_text
823         if help_text is None:
824             # They specified -h, but there was no Help() inside the
825             # SConscript files.  Give them the options usage.
826             raise SConsPrintHelpException
827         else:
828             print help_text
829             print "Use scons -H for help about command-line options."
830         exit_status = 0
831         return
832
833     # Change directory to the top-level SConstruct directory, then tell
834     # the Node.FS subsystem that we're all done reading the SConscript
835     # files and calling Repository() and BuildDir() and changing
836     # directories and the like, so it can go ahead and start memoizing
837     # the string values of file system nodes.
838
839     fs.chdir(fs.Top)
840
841     SCons.Node.FS.save_strings(1)
842
843     # Now that we've read the SConscripts we can set the options
844     # that are SConscript settable:
845     SCons.Node.implicit_cache = options.implicit_cache
846     SCons.Node.FS.set_duplicate(options.duplicate)
847     fs.set_max_drift(options.max_drift)
848
849     lookup_top = None
850     if targets or SCons.Script.BUILD_TARGETS != SCons.Script._build_plus_default:
851         # They specified targets on the command line or modified
852         # BUILD_TARGETS in the SConscript file(s), so if they used -u,
853         # -U or -D, we have to look up targets relative to the top,
854         # but we build whatever they specified.
855         if target_top:
856             lookup_top = fs.Dir(target_top)
857             target_top = None
858
859         targets = SCons.Script.BUILD_TARGETS
860     else:
861         # There are no targets specified on the command line,
862         # so if they used -u, -U or -D, we may have to restrict
863         # what actually gets built.
864         d = None
865         if target_top:
866             if options.climb_up == 1:
867                 # -u, local directory and below
868                 target_top = fs.Dir(target_top)
869                 lookup_top = target_top
870             elif options.climb_up == 2:
871                 # -D, all Default() targets
872                 target_top = None
873                 lookup_top = None
874             elif options.climb_up == 3:
875                 # -U, local SConscript Default() targets
876                 target_top = fs.Dir(target_top)
877                 def check_dir(x, target_top=target_top):
878                     if hasattr(x, 'cwd') and not x.cwd is None:
879                         cwd = x.cwd.srcnode()
880                         return cwd == target_top
881                     else:
882                         # x doesn't have a cwd, so it's either not a target,
883                         # or not a file, so go ahead and keep it as a default
884                         # target and let the engine sort it out:
885                         return 1                
886                 d = filter(check_dir, SCons.Script.DEFAULT_TARGETS)
887                 SCons.Script.DEFAULT_TARGETS[:] = d
888                 target_top = None
889                 lookup_top = None
890
891         targets = SCons.Script._Get_Default_Targets(d, fs)
892
893     if not targets:
894         sys.stderr.write("scons: *** No targets specified and no Default() targets found.  Stop.\n")
895         sys.exit(2)
896
897     def Entry(x, ltop=lookup_top, ttop=target_top, fs=fs):
898         if isinstance(x, SCons.Node.Node):
899             node = x
900         else:
901             node = None
902             # Why would ltop be None? Unfortunately this happens.
903             if ltop == None: ltop = ''
904             # Curdir becomes important when SCons is called with -u, -C,
905             # or similar option that changes directory, and so the paths
906             # of targets given on the command line need to be adjusted.
907             curdir = os.path.join(os.getcwd(), str(ltop))
908             for lookup in SCons.Node.arg2nodes_lookups:
909                 node = lookup(x, curdir=curdir)
910                 if node != None:
911                     break
912             if node is None:
913                 node = fs.Entry(x, directory=ltop, create=1)
914         if ttop and not node.is_under(ttop):
915             if isinstance(node, SCons.Node.FS.Dir) and ttop.is_under(node):
916                 node = ttop
917             else:
918                 node = None
919         return node
920
921     nodes = filter(None, map(Entry, targets))
922
923     task_class = BuildTask      # default action is to build targets
924     opening_message = "Building targets ..."
925     closing_message = "done building targets."
926     if options.keep_going:
927         failure_message = "done building targets (errors occurred during build)."
928     else:
929         failure_message = "building terminated because of errors."
930     if options.question:
931         task_class = QuestionTask
932     try:
933         if options.clean:
934             task_class = CleanTask
935             opening_message = "Cleaning targets ..."
936             closing_message = "done cleaning targets."
937             if options.keep_going:
938                 closing_message = "done cleaning targets (errors occurred during clean)."
939             else:
940                 failure_message = "cleaning terminated because of errors."
941     except AttributeError:
942         pass
943
944     if options.random:
945         def order(dependencies):
946             """Randomize the dependencies."""
947             import random
948             # This is cribbed from the implementation of
949             # random.shuffle() in Python 2.X.
950             d = dependencies
951             for i in xrange(len(d)-1, 0, -1):
952                 j = int(random.random() * (i+1))
953                 d[i], d[j] = d[j], d[i]
954             return d
955     else:
956         def order(dependencies):
957             """Leave the order of dependencies alone."""
958             return dependencies
959
960     progress_display("scons: " + opening_message)
961     if options.taskmastertrace_file == '-':
962         tmtrace = sys.stdout
963     elif options.taskmastertrace_file:
964         tmtrace = open(options.taskmastertrace_file, 'wb')
965     else:
966         tmtrace = None
967     taskmaster = SCons.Taskmaster.Taskmaster(nodes, task_class, order, tmtrace)
968
969     # Let the BuildTask objects get at the options to respond to the
970     # various print_* settings, tree_printer list, etc.
971     BuildTask.options = options
972
973     global num_jobs
974     num_jobs = options.num_jobs
975     jobs = SCons.Job.Jobs(num_jobs, taskmaster)
976     if num_jobs > 1 and jobs.num_jobs == 1:
977         msg = "parallel builds are unsupported by this version of Python;\n" + \
978               "\tignoring -j or num_jobs option.\n"
979         SCons.Warnings.warn(SCons.Warnings.NoParallelSupportWarning, msg)
980
981     memory_stats.append('before building targets:')
982     count_stats.append(('pre-', 'build'))
983
984     try:
985         jobs.run()
986     finally:
987         jobs.cleanup()
988         if exit_status:
989             progress_display("scons: " + failure_message)
990         else:
991             progress_display("scons: " + closing_message)
992         if not options.no_exec:
993             SCons.SConsign.write()
994
995     memory_stats.append('after building targets:')
996     count_stats.append(('post-', 'build'))
997
998 def _exec_main(parser, values):
999     sconsflags = os.environ.get('SCONSFLAGS', '')
1000     all_args = string.split(sconsflags) + sys.argv[1:]
1001
1002     options, args = parser.parse_args(all_args, values)
1003
1004     if type(options.debug) == type([]) and "pdb" in options.debug:
1005         import pdb
1006         pdb.Pdb().runcall(_main, parser)
1007     elif options.profile_file:
1008         from profile import Profile
1009
1010         # Some versions of Python 2.4 shipped a profiler that had the
1011         # wrong 'c_exception' entry in its dispatch table.  Make sure
1012         # we have the right one.  (This may put an unnecessary entry
1013         # in the table in earlier versions of Python, but its presence
1014         # shouldn't hurt anything).
1015         try:
1016             dispatch = Profile.dispatch
1017         except AttributeError:
1018             pass
1019         else:
1020             dispatch['c_exception'] = Profile.trace_dispatch_return
1021
1022         prof = Profile()
1023         try:
1024             prof.runcall(_main, parser)
1025         except SConsPrintHelpException, e:
1026             prof.dump_stats(options.profile_file)
1027             raise e
1028         except SystemExit:
1029             pass
1030         prof.dump_stats(options.profile_file)
1031     else:
1032         _main(parser)
1033
1034 def main():
1035     global OptionsParser
1036     global exit_status
1037     global first_command_start
1038
1039     parts = ["SCons by Steven Knight et al.:\n"]
1040     try:
1041         parts.append(version_string("script", __main__))
1042     except KeyboardInterrupt:
1043         raise
1044     except:
1045         # On Windows there is no scons.py, so there is no
1046         # __main__.__version__, hence there is no script version.
1047         pass 
1048     parts.append(version_string("engine", SCons))
1049     parts.append("__COPYRIGHT__")
1050     version = string.join(parts, '')
1051
1052     import SConsOptions
1053     parser = SConsOptions.Parser(version)
1054     values = SConsOptions.SConsValues(parser.get_default_values())
1055
1056     OptionsParser = parser
1057     
1058     try:
1059         _exec_main(parser, values)
1060     except SystemExit, s:
1061         if s:
1062             exit_status = s
1063     except KeyboardInterrupt:
1064         print "Build interrupted."
1065         sys.exit(2)
1066     except SyntaxError, e:
1067         _scons_syntax_error(e)
1068     except SCons.Errors.InternalError:
1069         _scons_internal_error()
1070     except SCons.Errors.UserError, e:
1071         _scons_user_error(e)
1072     except SConsPrintHelpException:
1073         parser.print_help()
1074         exit_status = 0
1075     except:
1076         # An exception here is likely a builtin Python exception Python
1077         # code in an SConscript file.  Show them precisely what the
1078         # problem was and where it happened.
1079         SCons.Script._SConscript.SConscript_exception()
1080         sys.exit(2)
1081
1082     memory_stats.print_stats()
1083     count_stats.print_stats()
1084
1085     if print_objects:
1086         SCons.Debug.listLoggedInstances('*')
1087         #SCons.Debug.dumpLoggedInstances('*')
1088
1089     if print_memoizer:
1090         SCons.Memoize.Dump("Memoizer (memory cache) hits and misses:")
1091
1092     # Dump any development debug info that may have been enabled.
1093     # These are purely for internal debugging during development, so
1094     # there's no need to control them with --debug= options; they're
1095     # controlled by changing the source code.
1096     SCons.Debug.dump_caller_counts()
1097     SCons.Taskmaster.dump_stats()
1098
1099     if print_time:
1100         total_time = time.time() - SCons.Script.start_time
1101         if num_jobs == 1:
1102             ct = cumulative_command_time
1103         else:
1104             if last_command_end is None or first_command_start is None:
1105                 ct = 0.0
1106             else:
1107                 ct = last_command_end - first_command_start
1108         scons_time = total_time - sconscript_time - ct
1109         print "Total build time: %f seconds"%total_time
1110         print "Total SConscript file execution time: %f seconds"%sconscript_time
1111         print "Total SCons execution time: %f seconds"%scons_time
1112         print "Total command execution time: %f seconds"%ct
1113
1114     sys.exit(exit_status)