Add SourceSignatures() and TargetSignatures() environment methods.
[scons.git] / src / engine / SCons / Script / SConscript.py
1 """SCons.Script.SConscript
2
3 This module defines the Python API provided to SConscript and SConstruct 
4 files.
5
6 """
7
8 #
9 # __COPYRIGHT__
10 #
11 # Permission is hereby granted, free of charge, to any person obtaining
12 # a copy of this software and associated documentation files (the
13 # "Software"), to deal in the Software without restriction, including
14 # without limitation the rights to use, copy, modify, merge, publish,
15 # distribute, sublicense, and/or sell copies of the Software, and to
16 # permit persons to whom the Software is furnished to do so, subject to
17 # the following conditions:
18 #
19 # The above copyright notice and this permission notice shall be included
20 # in all copies or substantial portions of the Software.
21 #
22 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
23 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
24 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 #
30
31 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
32
33 import SCons.Action
34 import SCons.Builder
35 import SCons.Defaults
36 import SCons.Environment
37 import SCons.Errors
38 import SCons.Node
39 import SCons.Node.FS
40 import SCons.Node.Python
41 import SCons.Platform
42 import SCons.SConf
43 import SCons.Script
44 import SCons.Util
45 import SCons.Options
46 import SCons
47 import SCons.Node.Alias
48
49 import os
50 import os.path
51 import re
52 import string
53 import sys
54 import traceback
55
56 def do_nothing(text): pass
57 HelpFunction = do_nothing
58
59 arguments = {}
60 launch_dir = os.path.abspath(os.curdir)
61
62 # global exports set by Export():
63 global_exports = {}
64
65 # chdir flag
66 sconscript_chdir = 1
67
68 def SConscriptChdir(flag):
69     global sconscript_chdir
70     sconscript_chdir = flag
71
72 def _scons_add_args(alist):
73     global arguments
74     for arg in alist:
75         a, b = string.split(arg, '=', 2)
76         arguments[a] = b
77
78 def get_calling_namespaces():
79     """Return the locals and globals for the function that called
80     into this module in the current callstack."""
81     try: 1/0
82     except: frame = sys.exc_info()[2].tb_frame
83     
84     while frame.f_globals.get("__name__") == __name__: frame = frame.f_back
85
86     return frame.f_locals, frame.f_globals
87
88     
89 def compute_exports(exports):
90     """Compute a dictionary of exports given one of the parameters
91     to the Export() function or the exports argument to SConscript()."""
92
93     exports = SCons.Util.Split(exports)
94     loc, glob = get_calling_namespaces()
95
96     retval = {}
97     try:
98         for export in exports:
99             if SCons.Util.is_Dict(export):
100                 retval.update(export)
101             else:
102                 try:
103                     retval[export] = loc[export]
104                 except KeyError:
105                     retval[export] = glob[export]
106     except KeyError, x:
107         raise SCons.Errors.UserError, "Export of non-existant variable '%s'"%x
108
109     return retval
110     
111
112 class Frame:
113     """A frame on the SConstruct/SConscript call stack"""
114     def __init__(self, exports, sconscript):
115         self.globals = BuildDefaultGlobals()
116         self.retval = None 
117         self.prev_dir = SCons.Node.FS.default_fs.getcwd()
118         self.exports = compute_exports(exports)  # exports from the calling SConscript
119         # make sure the sconscript attr is a Node.
120         if isinstance(sconscript, SCons.Node.Node):
121             self.sconscript = sconscript
122         else:
123             self.sconscript = SCons.Node.FS.default_fs.File(str(sconscript))
124         
125 # the SConstruct/SConscript call stack:
126 stack = []
127
128 # For documentation on the methods in this file, see the scons man-page
129
130 def Return(*vars):
131     retval = []
132     try:
133         for var in vars:
134             for v in string.split(var):
135                 retval.append(stack[-1].globals[v])
136     except KeyError, x:
137         raise SCons.Errors.UserError, "Return of non-existant variable '%s'"%x
138         
139     if len(retval) == 1:
140         stack[-1].retval = retval[0]
141     else:
142         stack[-1].retval = tuple(retval)
143
144 # This function is responsible for converting the parameters passed to
145 # SConscript() calls into a list of files and export variables.  If the
146 # parameters are invalid, throws SCons.Errors.UserError. Returns a tuple
147 # (l, e) where l is a list of SConscript filenames and e is a list of
148 # exports.
149
150 def GetSConscriptFilenames(ls, kw):
151     exports = []
152
153     if len(ls) == 0:
154         try:
155             dirs = kw["dirs"]
156         except KeyError:
157             raise SCons.Errors.UserError, \
158                   "Invalid SConscript usage - no parameters"
159
160         if not SCons.Util.is_List(dirs):
161             dirs = [ dirs ]
162         dirs = map(str, dirs)
163
164         name = kw.get('name', 'SConscript')
165
166         files = map(lambda n, name = name: os.path.join(n, name), dirs)
167
168     elif len(ls) == 1:
169
170         files = ls[0]
171
172     elif len(ls) == 2:
173
174         files   = ls[0]
175         exports = SCons.Util.Split(ls[1])
176
177     else:
178
179         raise SCons.Errors.UserError, \
180               "Invalid SConscript() usage - too many arguments"
181
182     if not SCons.Util.is_List(files):
183         files = [ files ]
184
185     if kw.get('exports'):
186         exports.extend(SCons.Util.Split(kw['exports']))
187
188     build_dir = kw.get('build_dir')
189     if build_dir:
190         if len(files) != 1:
191             raise SCons.Errors.UserError, \
192                 "Invalid SConscript() usage - can only specify one SConscript with a build_dir"
193         duplicate = kw.get('duplicate', 1)
194         src_dir = kw.get('src_dir')
195         if not src_dir:
196             src_dir, fname = os.path.split(str(files[0]))
197         else:
198             if not isinstance(src_dir, SCons.Node.Node):
199                 src_dir = SCons.Node.FS.default_fs.Dir(src_dir)
200             fn = files[0]
201             if not isinstance(fn, SCons.Node.Node):
202                 fn = SCons.Node.FS.default_fs.File(fn)
203             if fn.is_under(src_dir):
204                 # Get path relative to the source directory.
205                 fname = fn.get_path(src_dir)
206             else:
207                 # Fast way to only get the terminal path component of a Node.
208                 fname = fn.get_path(fn.dir)
209         BuildDir(build_dir, src_dir, duplicate)
210         files = [os.path.join(str(build_dir), fname)]
211
212     return (files, exports)
213
214 def SConscript(*ls, **kw):
215     files, exports = GetSConscriptFilenames(ls, kw)
216
217     default_fs = SCons.Node.FS.default_fs
218     top = default_fs.Top
219     sd = default_fs.SConstruct_dir.rdir()
220
221     # evaluate each SConscript file
222     results = []
223     for fn in files:
224         stack.append(Frame(exports,fn))
225         old_sys_path = sys.path
226         try:
227             if fn == "-":
228                 exec sys.stdin in stack[-1].globals
229             else:
230                 if isinstance(fn, SCons.Node.Node):
231                     f = fn
232                 else:
233                     f = default_fs.File(str(fn))
234                 _file_ = None
235
236                 # Change directory to the top of the source
237                 # tree to make sure the os's cwd and the cwd of
238                 # SCons.Node.FS.default_fs match so we can open the
239                 # SConscript.
240                 default_fs.chdir(top, change_os_dir=1)
241                 if f.rexists():
242                     _file_ = open(f.rstr(), "r")
243                 elif f.has_src_builder():
244                     # The SConscript file apparently exists in a source
245                     # code management system.  Build it, but then clear
246                     # the builder so that it doesn't get built *again*
247                     # during the actual build phase.
248                     f.build()
249                     f.builder_set(None)
250                     s = str(f)
251                     if os.path.exists(s):
252                         _file_ = open(s, "r")
253                 if _file_:
254                     # Chdir to the SConscript directory.  Use a path
255                     # name relative to the SConstruct file so that if
256                     # we're using the -f option, we're essentially
257                     # creating a parallel SConscript directory structure
258                     # in our local directory tree.
259                     #
260                     # XXX This is broken for multiple-repository cases
261                     # where the SConstruct and SConscript files might be
262                     # in different Repositories.  For now, cross that
263                     # bridge when someone comes to it.
264                     ldir = default_fs.Dir(f.dir.get_path(sd))
265                     try:
266                         default_fs.chdir(ldir, change_os_dir=sconscript_chdir)
267                     except OSError:
268                         # There was no local directory, so we should be
269                         # able to chdir to the Repository directory.
270                         # Note that we do this directly, not through
271                         # default_fs.chdir(), because we still need to
272                         # interpret the stuff within the SConscript file
273                         # relative to where we are logically.
274                         default_fs.chdir(ldir, change_os_dir=0)
275                         os.chdir(f.rfile().dir.get_abspath())
276
277                     # Append the SConscript directory to the beginning
278                     # of sys.path so Python modules in the SConscript
279                     # directory can be easily imported.
280                     sys.path = [ f.dir.get_abspath() ] + sys.path
281
282                     # This is the magic line that actually reads up and
283                     # executes the stuff in the SConscript file.  We
284                     # look for the "exec _file_ " from the beginning
285                     # of this line to find the right stack frame (the
286                     # next one) describing the SConscript file and line
287                     # number that creates a node.
288                     exec _file_ in stack[-1].globals
289                 else:
290                     sys.stderr.write("Ignoring missing SConscript '%s'\n" %
291                                      f.path)
292                 
293         finally:
294             sys.path = old_sys_path
295             frame = stack.pop()
296             try:
297                 default_fs.chdir(frame.prev_dir, change_os_dir=sconscript_chdir)
298             except OSError:
299                 # There was no local directory, so chdir to the
300                 # Repository directory.  Like above, we do this
301                 # directly.
302                 default_fs.chdir(frame.prev_dir, change_os_dir=0)
303                 os.chdir(frame.prev_dir.rdir().get_abspath())
304
305             results.append(frame.retval)
306
307     # if we only have one script, don't return a tuple
308     if len(results) == 1:
309         return results[0]
310     else:
311         return tuple(results)
312
313 def is_our_exec_statement(line):
314     return not line is None and line[:12] == "exec _file_ "
315
316 def SConscript_exception(file=sys.stderr):
317     """Print an exception stack trace just for the SConscript file(s).
318     This will show users who have Python errors where the problem is,
319     without cluttering the output with all of the internal calls leading
320     up to where we exec the SConscript."""
321     stack = traceback.extract_tb(sys.exc_traceback)
322     last_text = ""
323     i = 0
324     for frame in stack:
325         if is_our_exec_statement(last_text):
326             break
327         i = i + 1
328         last_text = frame[3]
329     type = str(sys.exc_type)
330     if type[:11] == "exceptions.":
331         type = type[11:]
332     file.write('%s: %s:\n' % (type, sys.exc_value))
333     for fname, line, func, text in stack[i:]:
334         file.write('  File "%s", line %d:\n' % (fname, line))
335         file.write('    %s\n' % text)
336
337 def annotate(node):
338     """Annotate a node with the stack frame describing the
339     SConscript file and line number that created it."""
340     stack = traceback.extract_stack()
341     last_text = ""
342     for frame in stack:
343         # If the script text of the previous frame begins with the
344         # magic "exec _file_ " string, then this frame describes the
345         # SConscript file and line number that caused this node to be
346         # created.  Record the tuple and carry on.
347         if is_our_exec_statement(last_text):
348             node.creator = frame
349             return
350         last_text = frame[3]
351
352 # The following line would cause each Node to be annotated using the
353 # above function.  Unfortunately, this is a *huge* performance hit, so
354 # leave this disabled until we find a more efficient mechanism.
355 #SCons.Node.Annotate = annotate
356
357 def Help(text):
358     HelpFunction(text)
359
360 def BuildDir(build_dir, src_dir, duplicate=1):
361     SCons.Node.FS.default_fs.BuildDir(build_dir, src_dir, duplicate)
362
363 def GetBuildPath(files):
364     nodes = SCons.Node.arg2nodes(files, SCons.Node.FS.default_fs.Entry)
365     ret = map(str, nodes)
366     if len(ret) == 1:
367         return ret[0]
368     return ret
369
370 def Export(*vars):
371     for var in vars:
372         global_exports.update(compute_exports(var))
373
374 def Import(*vars):
375     try:
376         for var in vars:
377             var = SCons.Util.Split(var)
378             for v in var:
379                 if v == '*':
380                     stack[-1].globals.update(global_exports)
381                     stack[-1].globals.update(stack[-1].exports)
382                 else:
383                     if stack[-1].exports.has_key(v):
384                         stack[-1].globals[v] = stack[-1].exports[v]
385                     else:
386                         stack[-1].globals[v] = global_exports[v]
387     except KeyError,x:
388         raise SCons.Errors.UserError, "Import of non-existant variable '%s'"%x
389
390 def GetLaunchDir():
391     return launch_dir
392
393 def SetBuildSignatureType(type):
394     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
395                         "The SetBuildSignatureType() function has been deprecated;\n" +\
396                         "\tuse the TargetSignatures() function instead.")
397     SCons.Defaults.DefaultEnvironment().TargetSignatures(type)
398
399 def SetContentSignatureType(type):
400     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
401                         "The SetContentSignatureType() function has been deprecated;\n" +\
402                         "\tuse the SourceSignatures() function instead.")
403     SCons.Defaults.DefaultEnvironment().SourceSignatures(type)
404
405 class Options(SCons.Options.Options):
406     def __init__(self, files=None, args=arguments):
407         SCons.Options.Options.__init__(self, files, args)
408
409 def CheckVersion(major, minor, version_string):
410     """Return 0 if 'major' and 'minor' are greater than the version
411     in 'version_string', and 1 otherwise."""
412     try:
413         v_major, v_minor, v_micro, release, serial = sys.version_info
414     except AttributeError:
415         version = string.split(string.split(version_string, ' ')[0], '.')
416         v_major = int(version[0])
417         v_minor = int(re.match('\d+', version[1]).group())
418     if major > v_major or (major == v_major and minor > v_minor):
419         return 0
420     else:
421         return 1
422
423 def EnsureSConsVersion(major, minor):
424     """Exit abnormally if the SCons version is not late enough."""
425     if not CheckVersion(major,minor,SCons.__version__):
426         print "SCons %d.%d or greater required, but you have SCons %s" %(major,minor,SCons.__version__)
427         sys.exit(2)
428
429 def EnsurePythonVersion(major, minor):
430     """Exit abnormally if the Python version is not late enough."""
431     if not CheckVersion(major,minor,sys.version):
432         v = string.split(sys.version, " ", 1)[0]
433         print "Python %d.%d or greater required, but you have Python %s" %(major,minor,v)
434         sys.exit(2)
435
436 def GetJobs():
437     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
438                         "The GetJobs() function has been deprecated;\n" +\
439                         "\tuse GetOption('num_jobs') instead.")
440
441     return GetOption('num_jobs')
442  
443 def SetJobs(num):
444     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
445                         "The SetJobs() function has been deprecated;\n" +\
446                         "\tuse SetOption('num_jobs', num) instead.")
447     SetOption('num_jobs', num)
448
449 def Exit(value=0):
450     sys.exit(value)
451
452
453 def Alias(name):
454     alias = SCons.Node.Alias.default_ans.lookup(name)
455     if alias is None:
456         alias = SCons.Node.Alias.default_ans.Alias(name)
457     return alias
458
459 def SetOption(name, value):
460     SCons.Script.ssoptions.set(name, value)
461
462 def GetOption(name):
463     return SCons.Script.ssoptions.get(name)
464
465 def SConsignFile(name=".sconsign.dbm"):
466     import SCons.Sig
467     if not os.path.isabs(name):
468         sd = str(SCons.Node.FS.default_fs.SConstruct_dir)
469         name = os.path.join(sd, name)
470     SCons.Sig.SConsignFile(name)
471
472 def BuildDefaultGlobals():
473     """
474     Create a dictionary containing all the default globals for 
475     SConstruct and SConscript files.
476     """
477
478     globals = {}
479     globals['Action']            = SCons.Action.Action
480     globals['Alias']             = Alias
481     globals['ARGUMENTS']         = arguments
482     globals['BuildDir']          = BuildDir
483     globals['Builder']           = SCons.Builder.Builder
484     globals['CacheDir']          = SCons.Node.FS.default_fs.CacheDir
485     globals['Configure']         = SCons.SConf.SConf
486     globals['CScan']             = SCons.Defaults.CScan
487     globals['DefaultEnvironment'] = SCons.Defaults.DefaultEnvironment
488     globals['Dir']               = SCons.Node.FS.default_fs.Dir
489     globals['EnsurePythonVersion'] = EnsurePythonVersion
490     globals['EnsureSConsVersion'] = EnsureSConsVersion
491     globals['Environment']       = SCons.Environment.Environment
492     globals['Exit']              = Exit
493     globals['Export']            = Export
494     globals['File']              = SCons.Node.FS.default_fs.File
495     globals['GetBuildPath']      = GetBuildPath
496     globals['GetCommandHandler'] = SCons.Action.GetCommandHandler
497     globals['GetJobs']           = GetJobs
498     globals['GetLaunchDir']      = GetLaunchDir
499     globals['GetOption']         = GetOption    
500     globals['Help']              = Help
501     globals['Import']            = Import
502     globals['Literal']           = SCons.Util.Literal
503     globals['Options']           = Options
504     globals['ParseConfig']       = SCons.Util.ParseConfig
505     globals['Platform']          = SCons.Platform.Platform
506     globals['Repository']        = SCons.Node.FS.default_fs.Repository
507     globals['Return']            = Return
508     globals['SConscript']        = SConscript
509     globals['SConscriptChdir']   = SConscriptChdir
510     globals['SConsignFile']      = SConsignFile
511     globals['Scanner']           = SCons.Scanner.Base
512     globals['SetBuildSignatureType'] = SetBuildSignatureType
513     globals['SetCommandHandler'] = SCons.Action.SetCommandHandler
514     globals['SetContentSignatureType'] = SetContentSignatureType
515     globals['SetJobs']           = SetJobs
516     globals['SetOption']         = SetOption
517     globals['Split']             = SCons.Util.Split
518     globals['Tool']              = SCons.Tool.Tool
519     globals['Value']             = SCons.Node.Python.Value
520     globals['WhereIs']           = SCons.Util.WhereIs
521
522     class DefaultEnvironmentCall:
523         """ """
524         def __init__(self, method_name):
525             self.method_name = method_name
526         def __call__(self, *args, **kw):
527             method = getattr(SCons.Defaults.DefaultEnvironment(),
528                              self.method_name)
529             return apply(method, args, kw)
530
531     EnvironmentMethods = [
532         'AddPostAction',
533         'AddPreAction',
534         'AlwaysBuild',
535         'Clean',
536         'Command',
537         'Default',
538         'Depends',
539         'FindFile',
540         'Ignore',
541         'Install',
542         'InstallAs',
543         'Local',
544         'Precious',
545         'SideEffect',
546         'SourceCode',
547         'SourceSignatures',
548         'TargetSignatures',
549     ]
550
551     for name in EnvironmentMethods:
552         globals[name] = DefaultEnvironmentCall(name)
553
554     return globals