Handle build errors.
[scons.git] / src / script / scons.py
1 #! /usr/bin/env python
2 #
3 # SCons
4 #
5
6 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
7
8 import getopt
9 import os
10 import os.path
11 import string
12 import sys
13 import traceback
14
15 import SCons.Node.FS
16 import SCons.Job
17 from SCons.Errors import *
18 import SCons.Sig
19 import SCons.Sig.MD5
20
21 #
22 # Modules and classes that we don't use directly in this script, but
23 # which we want available for use in SConstruct and SConscript files.
24 #
25 from SCons.Environment import Environment
26 from SCons.Builder import Builder
27
28 class Task:
29     "XXX: this is here only until the build engine is implemented"
30
31     def __init__(self, target):
32         self.target = target
33
34     def execute(self):
35         try:
36             self.target.build()
37         except BuildError, e:
38             sys.stderr.write("scons: *** [%s] Error %d\n" % (e.node, e.stat))
39             raise
40
41
42
43 class Taskmaster:
44     "XXX: this is here only until the build engine is implemented"
45
46     def __init__(self, targets, calc):
47         self.targets = targets
48         self.calc = calc
49         self.num_iterated = 0
50
51
52     def next_task(self):
53         while self.num_iterated < len(self.targets):
54             t = self.targets[self.num_iterated]
55             self.num_iterated = self.num_iterated + 1
56             if self.calc.current(t):
57                 print 'scons: "%s" is up to date.' % t
58             else:
59                 return Task(t)
60         return None
61
62     def is_blocked(self):
63         return 0
64
65     def executed(self, task):
66         pass
67
68     def failed(self, task):
69         self.num_iterated = len(self.targets)
70
71
72 # Global variables
73
74 default_targets = []
75 include_dirs = []
76 help_option = None
77 num_jobs = 1
78 scripts = []
79
80 # utility functions
81
82 def _scons_syntax_error(e):
83     """Handle syntax errors. Print out a message and show where the error
84     occurred.
85     """
86     etype, value, tb = sys.exc_info()
87     lines = traceback.format_exception_only(etype, value)
88     for line in lines:
89         sys.stderr.write(line+'\n')
90
91 def _scons_user_error(e):
92     """Handle user errors. Print out a message and a description of the
93     error, along with the line number and routine where it occured.
94     """
95     etype, value, tb = sys.exc_info()
96     while tb.tb_next is not None:
97         tb = tb.tb_next
98     lineno = traceback.tb_lineno(tb)
99     filename = tb.tb_frame.f_code.co_filename
100     routine = tb.tb_frame.f_code.co_name
101     sys.stderr.write("\nSCons error: %s\n" % value)
102     sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
103
104 def _scons_user_warning(e):
105     """Handle user warnings. Print out a message and a description of
106     the warning, along with the line number and routine where it occured.
107     """
108     etype, value, tb = sys.exc_info()
109     while tb.tb_next is not None:
110         tb = tb.tb_next
111     lineno = traceback.tb_lineno(tb)
112     filename = tb.tb_frame.f_code.co_filename
113     routine = tb.tb_frame.f_code.co_name
114     sys.stderr.write("\nSCons warning: %s\n" % e)
115     sys.stderr.write('File "%s", line %d, in %s\n' % (filename, lineno, routine))
116
117 def _scons_other_errors():
118     """Handle all errors but user errors. Print out a message telling
119     the user what to do in this case and print a normal trace.
120     """
121     print 'other errors'
122     traceback.print_exc()
123
124
125
126 def Conscript(filename):
127     global scripts
128     scripts.append(filename)
129
130 def Default(*targets):
131     for t in targets:
132         for s in string.split(t):
133             default_targets.append(s)
134
135 def Help(text):
136     global help_option
137     if help_option == 'h':
138         print text
139         print "Use scons -H for help about command-line options."
140         sys.exit(0)
141
142
143
144 #
145 # After options are initialized, the following variables are
146 # filled in:
147 #
148 option_list = []        # list of Option objects
149 short_opts = ""         # string of short (single-character) options
150 long_opts = []          # array of long (--) options
151 opt_func = {}           # mapping of option strings to functions
152
153 def options_init():
154     """Initialize command-line options processing.
155     
156     This is in a subroutine mainly so we can easily single-step over
157     it in the debugger.
158     """
159
160     class Option:
161         """Class for command-line option information.
162
163         This exists to provide a central location for everything
164         describing a command-line option, so that we can change
165         options without having to update the code to handle the
166         option in one place, the -h help message in another place,
167         etc.  There are no methods here, only attributes.
168
169         You can initialize an Option with the following:
170
171         func    The function that will be called when this
172                 option is processed on the command line.
173                 Calling sequence is:
174
175                         func(opt, arg)
176
177                 If there is no func, then this Option probably
178                 stores an optstring to be printed.
179
180         helpline
181                 The string to be printed in -h output.  If no
182                 helpline is specified but a help string is
183                 specified (the usual case), a helpline will be
184                 constructed automatically from the short, long,
185                 arg, and help attributes.  (In practice, then,
186                 setting helpline without setting func allows you
187                 to print arbitrary lines of text in the -h
188                 output.)
189
190         short   The string for short, single-hyphen
191                 command-line options.
192                 Do not include the hyphen:
193
194                         'a' for -a, 'xy' for -x and -y, etc.
195
196         long    An array of strings for long, double-hyphen
197                 command-line options.  Do not include
198                 the hyphens:
199
200                         ['my-option', 'verbose']
201
202         arg     If this option takes an argument, this string
203                 specifies how you want it to appear in the
204                 -h output ('DIRECTORY', 'FILE', etc.).
205
206         help    The help string that will be printed for
207                 this option in the -h output.  Must be
208                 49 characters or fewer.
209
210         future  If non-zero, this indicates that this feature
211                 will be supported in a future release, not
212                 the currently planned one.  SCons will
213                 recognize the option, but it won't show up
214                 in the -h output.
215
216         The following attribute is derived from the supplied attributes:
217
218         optstring
219                 A string, with hyphens, describing the flags
220                 for this option, as constructed from the
221                 specified short, long and arg attributes.
222
223         All Option objects are stored in the global option_list list,
224         in the order in which they're created.  This is the list
225         that's used to generate -h output, so the order in which the
226         objects are created is the order in which they're printed.
227
228         The upshot is that specifying a command-line option and having
229         everything work correctly is a matter of defining a function to
230         process its command-line argument (set the right flag, update
231         the right value), and then creating an appropriate Option object
232         at the correct point in the code below.
233         """
234
235         def __init__(self, func = None, helpline = None,
236                  short = None, long = None, arg = None,
237                  help = None, future = None):
238             self.func = func
239             self.short = short
240             self.long = long
241             self.arg = arg
242             self.help = help
243             opts = []
244             if self.short:
245                 for c in self.short:
246                     if arg:
247                         c = c + " " + arg
248                     opts = opts + ['-' + c]
249             if self.long:
250                 l = self.long
251                 if arg:
252                     l = map(lambda x,a=arg: x + "=" + a, self.long)
253                 opts = opts + map(lambda x: '--' + x, l)
254             self.optstring = string.join(opts, ', ')
255             if helpline:
256                 self.helpline = helpline
257             elif help and not future:
258                 if len(self.optstring) <= 26:
259                     sep = " " * (28 - len(self.optstring))
260                 else:
261                     sep = self.helpstring = "\n" + " " * 30
262                 self.helpline = "  " + self.optstring + sep + self.help
263             else:
264                 self.helpline = None
265             global option_list
266             option_list.append(self)
267
268     # Generic routine for to-be-written options, used by multiple
269     # options below.
270
271     def opt_not_yet(opt, arg):
272         sys.stderr.write("Warning:  the %s option is not yet implemented\n"
273                           % opt)
274
275     # In the following instantiations, the help string should be no
276     # longer than 49 characters.  Use the following as a guide:
277     #   help = "1234567890123456789012345678901234567890123456789"
278
279     def opt_ignore(opt, arg):
280         sys.stderr.write("Warning:  ignoring %s option\n" % opt)
281
282     Option(func = opt_ignore,
283         short = 'bmSt', long = ['no-keep-going', 'stop', 'touch'],
284         help = "Ignored for compatibility.")
285
286     Option(func = opt_not_yet,
287         short = 'c', long = ['clean', 'remove'],
288         help = "Remove specified targets and dependencies.")
289
290     Option(func = opt_not_yet, future = 1,
291         long = ['cache-disable', 'no-cache'],
292         help = "Do not retrieve built targets from Cache.")
293
294     Option(func = opt_not_yet, future = 1,
295         long = ['cache-force', 'cache-populate'],
296         help = "Copy already-built targets into the Cache.")
297
298     Option(func = opt_not_yet, future = 1,
299         long = ['cache-show'],
300         help = "Print what would have built Cached targets.")
301
302     def opt_C(opt, arg):
303         try:
304             os.chdir(arg)
305         except:
306             sys.stderr.write("Could not change directory to 'arg'\n")
307
308     Option(func = opt_C,
309         short = 'C', long = ['directory'], arg = 'DIRECTORY',
310         help = "Change to DIRECTORY before doing anything.")
311
312     Option(func = opt_not_yet,
313         short = 'd',
314         help = "Print file dependency information.")
315
316     Option(func = opt_not_yet, future = 1,
317         long = ['debug'], arg = 'FLAGS',
318         help = "Print various types of debugging information.")
319
320     Option(func = opt_not_yet, future = 1,
321         short = 'e', long = ['environment-overrides'],
322         help = "Environment variables override makefiles.")
323
324     def opt_f(opt, arg):
325         global scripts
326         scripts.append(arg)
327
328     Option(func = opt_f,
329         short = 'f', long = ['file', 'makefile', 'sconstruct'], arg = 'FILE',
330         help = "Read FILE as the top-level SConstruct file.")
331
332     def opt_help(opt, arg):
333         global help_option
334         help_option = 'h'
335
336     Option(func = opt_help,
337         short = 'h', long = ['help'],
338         help = "Print defined help message, or this one.")
339
340     def opt_help_options(opt, arg):
341         global help_option
342         help_option = 'H'
343
344     Option(func = opt_help_options,
345         short = 'H', long = ['help-options'],
346         help = "Print this message and exit.")
347
348     Option(func = opt_not_yet,
349         short = 'i', long = ['ignore-errors'],
350         help = "Ignore errors from build actions.")
351
352     def opt_I(opt, arg):
353         global include_dirs
354         include_dirs = include_dirs + [arg]
355
356     Option(func = opt_I,
357         short = 'I', long = ['include-dir'], arg = 'DIRECTORY',
358         help = "Search DIRECTORY for imported Python modules.")
359
360     def opt_j(opt, arg):
361         global num_jobs
362         try:
363             num_jobs = int(arg)
364         except:
365             print UsageString()
366             sys.exit(1)
367
368         if num_jobs <= 0:
369             print UsageString()
370             sys.exit(1)
371
372     Option(func = opt_j,
373         short = 'j', long = ['jobs'], arg = 'N',
374         help = "Allow N jobs at once.")
375
376     Option(func = opt_not_yet,
377         short = 'k', long = ['keep-going'],
378         help = "Keep going when a target can't be made.")
379
380     Option(func = opt_not_yet, future = 1,
381         short = 'l', long = ['load-average', 'max-load'], arg = 'N',
382         help = "Don't start multiple jobs unless load is below N.")
383
384     Option(func = opt_not_yet, future = 1,
385         long = ['list-derived'],
386         help = "Don't build; list files that would be built.")
387
388     Option(func = opt_not_yet, future = 1,
389         long = ['list-actions'],
390         help = "Don't build; list files and build actions.")
391
392     Option(func = opt_not_yet, future = 1,
393         long = ['list-where'],
394         help = "Don't build; list files and where defined.")
395
396     def opt_n(opt, arg):
397         SCons.Builder.execute_actions = None
398
399     Option(func = opt_n,
400         short = 'n', long = ['no-exec', 'just-print', 'dry-run', 'recon'],
401         help = "Don't build; just print commands.")
402
403     Option(func = opt_not_yet, future = 1,
404         short = 'o', long = ['old-file', 'assume-old'], arg = 'FILE',
405         help = "Consider FILE to be old; don't rebuild it.")
406
407     Option(func = opt_not_yet, future = 1,
408         long = ['override'], arg = 'FILE',
409         help = "Override variables as specified in FILE.")
410
411     Option(func = opt_not_yet, future = 1,
412         short = 'p',
413         help = "Print internal environments/objects.")
414
415     Option(func = opt_not_yet, future = 1,
416         short = 'q', long = ['question'],
417         help = "Don't build; exit status says if up to date.")
418
419     Option(func = opt_not_yet, future = 1,
420         short = 'rR', long = ['no-builtin-rules', 'no-builtin-variables'],
421         help = "Clear default environments and variables.")
422
423     Option(func = opt_not_yet, future = 1,
424         long = ['random'],
425         help = "Build dependencies in random order.")
426
427     def opt_s(opt, arg):
428         SCons.Builder.print_actions = None
429
430     Option(func = opt_s,
431         short = 's', long = ['silent', 'quiet'],
432         help = "Don't print commands.")
433
434     Option(func = opt_not_yet, future = 1,
435         short = 'u', long = ['up', 'search-up'],
436         help = "Search up directory tree for SConstruct.")
437
438     def option_v(opt, arg):
439         print "SCons version __VERSION__, by Steven Knight et al."
440         print "Copyright 2001 Steven Knight"
441         sys.exit(0)
442
443     Option(func = option_v,
444         short = 'v', long = ['version'],
445         help = "Print the SCons version number and exit.")
446
447     Option(func = opt_not_yet, future = 1,
448         short = 'w', long = ['print-directory'],
449         help = "Print the current directory.")
450
451     Option(func = opt_not_yet, future = 1,
452         long = ['no-print-directory'],
453         help = "Turn off -w, even if it was turned on implicitly.")
454
455     Option(func = opt_not_yet, future = 1,
456         long = ['write-filenames'], arg = 'FILE',
457         help = "Write all filenames examined into FILE.")
458
459     Option(func = opt_not_yet, future = 1,
460         short = 'W', long = ['what-if', 'new-file', 'assume-new'], arg = 'FILE',
461         help = "Consider FILE to be changed.")
462
463     Option(func = opt_not_yet, future = 1,
464         long = ['warn-undefined-variables'],
465         help = "Warn when an undefined variable is referenced.")
466
467     Option(func = opt_not_yet, future = 1,
468         short = 'Y', long = ['repository'], arg = 'REPOSITORY',
469         help = "Search REPOSITORY for source and target files.")
470
471     global short_opts
472     global long_opts
473     global opt_func
474     for o in option_list:
475         if o.short:
476             if o.func:
477                 for c in o.short:
478                     opt_func['-' + c] = o.func
479             short_opts = short_opts + o.short
480             if o.arg:
481                 short_opts = short_opts + ":"
482         if o.long:
483             if o.func:
484                 for l in o.long:
485                     opt_func['--' + l] = o.func
486             if o.arg:
487                 long_opts = long_opts + map(lambda a: a + "=", o.long)
488             else:
489                 long_opts = long_opts + o.long
490
491 options_init()
492
493
494
495 def UsageString():
496     help_opts = filter(lambda x: x.helpline, option_list)
497     s = "Usage: scons [OPTION] [TARGET] ...\n" + "Options:\n" + \
498         string.join(map(lambda x: x.helpline, help_opts), "\n") + "\n"
499     return s
500
501
502
503 def main():
504     global scripts, help_option, num_jobs
505
506     targets = []
507
508     # It looks like 2.0 changed the name of the exception class
509     # raised by getopt.
510     try:
511         getopt_err = getopt.GetoptError
512     except:
513         getopt_err = getopt.error
514
515     try:
516         cmd_opts, t = getopt.getopt(string.split(os.environ['SCONSFLAGS']),
517                                           short_opts, long_opts)
518     except KeyError:
519         # It's all right if there's no SCONSFLAGS environment variable.
520         pass
521     except getopt_err, x:
522         _scons_user_warning("SCONSFLAGS " + x)
523     else:
524         for opt, arg in cmd_opts:
525             opt_func[opt](opt, arg)
526
527     try:
528         cmd_opts, targets = getopt.getopt(sys.argv[1:], short_opts, long_opts)
529     except getopt_err, x:
530         _scons_user_error(x)
531     else:
532         for opt, arg in cmd_opts:
533             opt_func[opt](opt, arg)
534
535     if not scripts:
536         for file in ['SConstruct', 'Sconstruct', 'sconstruct']:
537             if os.path.isfile(file):
538                 scripts.append(file)
539                 break
540
541     if help_option == 'H':
542         print UsageString()
543         sys.exit(0)
544
545     if not scripts:
546         if help_option == 'h':
547             # There's no SConstruct, but they specified either -h or
548             # -H.  Give them the options usage now, before we fail
549             # trying to read a non-existent SConstruct file.
550             print UsageString()
551             sys.exit(0)
552         else:
553             raise UserError, "No SConstruct file found."
554
555     # XXX The commented-out code here adds any "scons" subdirs in anything
556     # along sys.path to sys.path.  This was an attempt at setting up things
557     # so we can import "node.FS" instead of "SCons.Node.FS".  This doesn't
558     # quite fit our testing methodology, though, so save it for now until
559     # the right solutions pops up.
560     #
561     #dirlist = []
562     #for dir in sys.path:
563     #    scons = os.path.join(dir, 'scons')
564     #    if os.path.isdir(scons):
565     #     dirlist = dirlist + [scons]
566     #    dirlist = dirlist + [dir]
567     #
568     #sys.path = dirlist
569
570     sys.path = include_dirs + sys.path
571
572     while scripts:
573         file, scripts = scripts[0], scripts[1:]
574         if file == "-":
575             exec sys.stdin in globals()
576         else:
577             try:
578                 f = open(file, "r")
579             except IOError, s:
580                 sys.stderr.write("Ignoring missing script '%s'\n" % file)
581             else:
582                 exec f in globals()
583
584     if help_option == 'h':
585         # They specified -h, but there was no Help() inside the
586         # SConscript files.  Give them the options usage.
587         print UsageString()
588         sys.exit(0)
589
590     if not targets:
591         targets = default_targets
592
593     nodes = map(lambda x: SCons.Node.FS.default_fs.File(x), targets)
594     calc = SCons.Sig.Calculator(SCons.Sig.MD5)
595
596     taskmaster = Taskmaster(nodes, calc)
597
598     jobs = SCons.Job.Jobs(num_jobs, taskmaster)
599     jobs.start()
600     jobs.wait()
601
602     calc.write(nodes)
603
604 if __name__ == "__main__":
605     try:
606         main()
607     except SystemExit:
608         pass
609     except KeyboardInterrupt:
610         print "Build interrupted."
611     except SyntaxError, e:
612         _scons_syntax_error(e)
613     except UserError, e:
614         _scons_user_error(e)
615     except:
616         _scons_other_errors()