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