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