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