Expand construction variables on the values of SConscript() keyword arguments.
[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
34 import SCons.Action
35 import SCons.Builder
36 import SCons.Defaults
37 import SCons.Environment
38 import SCons.Errors
39 import SCons.Node
40 import SCons.Node.Alias
41 import SCons.Node.FS
42 import SCons.Node.Python
43 import SCons.Platform
44 import SCons.SConf
45 import SCons.Script
46 import SCons.Util
47 import SCons.Options
48
49 import os
50 import os.path
51 import re
52 import string
53 import sys
54 import traceback
55 import types
56
57 def do_nothing(text): pass
58 HelpFunction = do_nothing
59
60 arguments = {}
61 launch_dir = os.path.abspath(os.curdir)
62
63 # global exports set by Export():
64 global_exports = {}
65
66 # chdir flag
67 sconscript_chdir = 1
68
69 def SConscriptChdir(flag):
70     global sconscript_chdir
71     sconscript_chdir = flag
72
73 def _scons_add_args(alist):
74     global arguments
75     for arg in alist:
76         a, b = string.split(arg, '=', 2)
77         arguments[a] = b
78
79 def get_calling_namespaces():
80     """Return the locals and globals for the function that called
81     into this module in the current callstack."""
82     try: 1/0
83     except: frame = sys.exc_info()[2].tb_frame
84     
85     while frame.f_globals.get("__name__") == __name__: frame = frame.f_back
86
87     return frame.f_locals, frame.f_globals
88
89     
90 def compute_exports(exports):
91     """Compute a dictionary of exports given one of the parameters
92     to the Export() function or the exports argument to SConscript()."""
93
94     exports = SCons.Util.Split(exports)
95     loc, glob = get_calling_namespaces()
96
97     retval = {}
98     try:
99         for export in exports:
100             if SCons.Util.is_Dict(export):
101                 retval.update(export)
102             else:
103                 try:
104                     retval[export] = loc[export]
105                 except KeyError:
106                     retval[export] = glob[export]
107     except KeyError, x:
108         raise SCons.Errors.UserError, "Export of non-existant variable '%s'"%x
109
110     return retval
111     
112
113 class Frame:
114     """A frame on the SConstruct/SConscript call stack"""
115     def __init__(self, exports, sconscript):
116         self.globals = BuildDefaultGlobals()
117         self.retval = None 
118         self.prev_dir = SCons.Node.FS.default_fs.getcwd()
119         self.exports = compute_exports(exports)  # exports from the calling SConscript
120         # make sure the sconscript attr is a Node.
121         if isinstance(sconscript, SCons.Node.Node):
122             self.sconscript = sconscript
123         else:
124             self.sconscript = SCons.Node.FS.default_fs.File(str(sconscript))
125         
126 # the SConstruct/SConscript call stack:
127 stack = []
128
129 # For documentation on the methods in this file, see the scons man-page
130
131 def Return(*vars):
132     retval = []
133     try:
134         for var in vars:
135             for v in string.split(var):
136                 retval.append(stack[-1].globals[v])
137     except KeyError, x:
138         raise SCons.Errors.UserError, "Return of non-existant variable '%s'"%x
139         
140     if len(retval) == 1:
141         stack[-1].retval = retval[0]
142     else:
143         stack[-1].retval = tuple(retval)
144
145 # This function is responsible for converting the parameters passed to
146 # SConscript() calls into a list of files and export variables.  If the
147 # parameters are invalid, throws SCons.Errors.UserError. Returns a tuple
148 # (l, e) where l is a list of SConscript filenames and e is a list of
149 # exports.
150
151 def GetSConscriptFilenames(ls, kw):
152     exports = []
153
154     if len(ls) == 0:
155         try:
156             dirs = kw["dirs"]
157         except KeyError:
158             raise SCons.Errors.UserError, \
159                   "Invalid SConscript usage - no parameters"
160
161         if not SCons.Util.is_List(dirs):
162             dirs = [ dirs ]
163         dirs = map(str, dirs)
164
165         name = kw.get('name', 'SConscript')
166
167         files = map(lambda n, name = name: os.path.join(n, name), dirs)
168
169     elif len(ls) == 1:
170
171         files = ls[0]
172
173     elif len(ls) == 2:
174
175         files   = ls[0]
176         exports = SCons.Util.Split(ls[1])
177
178     else:
179
180         raise SCons.Errors.UserError, \
181               "Invalid SConscript() usage - too many arguments"
182
183     if not SCons.Util.is_List(files):
184         files = [ files ]
185
186     if kw.get('exports'):
187         exports.extend(SCons.Util.Split(kw['exports']))
188
189     build_dir = kw.get('build_dir')
190     if build_dir:
191         if len(files) != 1:
192             raise SCons.Errors.UserError, \
193                 "Invalid SConscript() usage - can only specify one SConscript with a build_dir"
194         duplicate = kw.get('duplicate', 1)
195         src_dir = kw.get('src_dir')
196         if not src_dir:
197             src_dir, fname = os.path.split(str(files[0]))
198         else:
199             if not isinstance(src_dir, SCons.Node.Node):
200                 src_dir = SCons.Node.FS.default_fs.Dir(src_dir)
201             fn = files[0]
202             if not isinstance(fn, SCons.Node.Node):
203                 fn = SCons.Node.FS.default_fs.File(fn)
204             if fn.is_under(src_dir):
205                 # Get path relative to the source directory.
206                 fname = fn.get_path(src_dir)
207             else:
208                 # Fast way to only get the terminal path component of a Node.
209                 fname = fn.get_path(fn.dir)
210         SCons.Node.FS.default_fs.BuildDir(build_dir, src_dir, duplicate)
211         files = [os.path.join(str(build_dir), fname)]
212
213     return (files, exports)
214
215 def _SConscript(fs, *ls, **kw):
216     files, exports = GetSConscriptFilenames(ls, kw)
217
218     top = fs.Top
219     sd = 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 = 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                 # fs match so we can open the SConscript.
239                 fs.chdir(top, change_os_dir=1)
240                 if f.rexists():
241                     _file_ = open(f.rstr(), "r")
242                 elif f.has_src_builder():
243                     # The SConscript file apparently exists in a source
244                     # code management system.  Build it, but then clear
245                     # the builder so that it doesn't get built *again*
246                     # during the actual build phase.
247                     f.build()
248                     f.builder_set(None)
249                     s = str(f)
250                     if os.path.exists(s):
251                         _file_ = open(s, "r")
252                 if _file_:
253                     # Chdir to the SConscript directory.  Use a path
254                     # name relative to the SConstruct file so that if
255                     # we're using the -f option, we're essentially
256                     # creating a parallel SConscript directory structure
257                     # in our local directory tree.
258                     #
259                     # XXX This is broken for multiple-repository cases
260                     # where the SConstruct and SConscript files might be
261                     # in different Repositories.  For now, cross that
262                     # bridge when someone comes to it.
263                     ldir = fs.Dir(f.dir.get_path(sd))
264                     try:
265                         fs.chdir(ldir, change_os_dir=sconscript_chdir)
266                     except OSError:
267                         # There was no local directory, so we should be
268                         # able to chdir to the Repository directory.
269                         # Note that we do this directly, not through
270                         # fs.chdir(), because we still need to
271                         # interpret the stuff within the SConscript file
272                         # relative to where we are logically.
273                         fs.chdir(ldir, change_os_dir=0)
274                         os.chdir(f.rfile().dir.get_abspath())
275
276                     # Append the SConscript directory to the beginning
277                     # of sys.path so Python modules in the SConscript
278                     # directory can be easily imported.
279                     sys.path = [ f.dir.get_abspath() ] + sys.path
280
281                     # This is the magic line that actually reads up and
282                     # executes the stuff in the SConscript file.  We
283                     # look for the "exec _file_ " from the beginning
284                     # of this line to find the right stack frame (the
285                     # next one) describing the SConscript file and line
286                     # number that creates a node.
287                     exec _file_ in stack[-1].globals
288                 else:
289                     sys.stderr.write("Ignoring missing SConscript '%s'\n" %
290                                      f.path)
291                 
292         finally:
293             sys.path = old_sys_path
294             frame = stack.pop()
295             try:
296                 fs.chdir(frame.prev_dir, change_os_dir=sconscript_chdir)
297             except OSError:
298                 # There was no local directory, so chdir to the
299                 # Repository directory.  Like above, we do this
300                 # directly.
301                 fs.chdir(frame.prev_dir, change_os_dir=0)
302                 os.chdir(frame.prev_dir.rdir().get_abspath())
303
304             results.append(frame.retval)
305
306     # if we only have one script, don't return a tuple
307     if len(results) == 1:
308         return results[0]
309     else:
310         return tuple(results)
311
312 def is_our_exec_statement(line):
313     return not line is None and line[:12] == "exec _file_ "
314
315 def SConscript_exception(file=sys.stderr):
316     """Print an exception stack trace just for the SConscript file(s).
317     This will show users who have Python errors where the problem is,
318     without cluttering the output with all of the internal calls leading
319     up to where we exec the SConscript."""
320     stack = traceback.extract_tb(sys.exc_traceback)
321     last_text = ""
322     i = 0
323     for frame in stack:
324         if is_our_exec_statement(last_text):
325             break
326         i = i + 1
327         last_text = frame[3]
328     type = str(sys.exc_type)
329     if type[:11] == "exceptions.":
330         type = type[11:]
331     file.write('%s: %s:\n' % (type, sys.exc_value))
332     for fname, line, func, text in stack[i:]:
333         file.write('  File "%s", line %d:\n' % (fname, line))
334         file.write('    %s\n' % text)
335
336 def annotate(node):
337     """Annotate a node with the stack frame describing the
338     SConscript file and line number that created it."""
339     stack = traceback.extract_stack()
340     last_text = ""
341     for frame in stack:
342         # If the script text of the previous frame begins with the
343         # magic "exec _file_ " string, then this frame describes the
344         # SConscript file and line number that caused this node to be
345         # created.  Record the tuple and carry on.
346         if is_our_exec_statement(last_text):
347             node.creator = frame
348             return
349         last_text = frame[3]
350
351 # The following line would cause each Node to be annotated using the
352 # above function.  Unfortunately, this is a *huge* performance hit, so
353 # leave this disabled until we find a more efficient mechanism.
354 #SCons.Node.Annotate = annotate
355
356 class SConsEnvironment(SCons.Environment.Base):
357     """An Environment subclass that contains all of the methods that
358     are particular to the wrapper SCons interface and which aren't
359     (or shouldn't be) part of the build engine itself.
360     """
361
362     #
363     # Private functions of an SConsEnvironment.
364     #
365
366     def _check_version(self, major, minor, version_string):
367         """Return 0 if 'major' and 'minor' are greater than the version
368         in 'version_string', and 1 otherwise."""
369         try:
370             v_major, v_minor, v_micro, release, serial = sys.version_info
371         except AttributeError:
372             version = string.split(string.split(version_string, ' ')[0], '.')
373             v_major = int(version[0])
374             v_minor = int(re.match('\d+', version[1]).group())
375         if major > v_major or (major == v_major and minor > v_minor):
376             return 0
377         else:
378             return 1
379
380     #
381     # Public functions of an SConsEnvironment.  These get
382     # entry points in the global name space so they can be called
383     # as global functions.
384     #
385
386     def EnsureSConsVersion(self, major, minor):
387         """Exit abnormally if the SCons version is not late enough."""
388         if not self._check_version(major,minor,SCons.__version__):
389             print "SCons %d.%d or greater required, but you have SCons %s" %(major,minor,SCons.__version__)
390             sys.exit(2)
391
392     def EnsurePythonVersion(self, major, minor):
393         """Exit abnormally if the Python version is not late enough."""
394         if not self._check_version(major,minor,sys.version):
395             v = string.split(sys.version, " ", 1)[0]
396             print "Python %d.%d or greater required, but you have Python %s" %(major,minor,v)
397             sys.exit(2)
398
399     def Exit(self, value=0):
400         sys.exit(value)
401
402     def Export(self, *vars):
403         for var in vars:
404             global_exports.update(compute_exports(var))
405
406     def GetLaunchDir(self):
407         global launch_dir
408         return launch_dir
409
410     def GetOption(self, name):
411         name = self.subst(name)
412         return SCons.Script.ssoptions.get(name)
413
414     def Help(self, text):
415         text = self.subst(text, raw=1)
416         HelpFunction(text)
417
418     def Import(self, *vars):
419         try:
420             for var in vars:
421                 var = SCons.Util.Split(var)
422                 for v in var:
423                     if v == '*':
424                         stack[-1].globals.update(global_exports)
425                         stack[-1].globals.update(stack[-1].exports)
426                     else:
427                         if stack[-1].exports.has_key(v):
428                             stack[-1].globals[v] = stack[-1].exports[v]
429                         else:
430                             stack[-1].globals[v] = global_exports[v]
431         except KeyError,x:
432             raise SCons.Errors.UserError, "Import of non-existant variable '%s'"%x
433
434     def SConscript(self, *ls, **kw):
435         ls = map(lambda l, self=self: self.subst(l), ls)
436         subst_kw = {}
437         for key, val in kw.items():
438             if SCons.Util.is_String(val):
439                 val = self.subst(val)
440             subst_kw[key] = val
441         return apply(_SConscript, [self.fs,] + ls, subst_kw)
442
443     def SetOption(self, name, value):
444         name = self.subst(name)
445         SCons.Script.ssoptions.set(name, value)
446
447 #
448 #
449 #
450 SCons.Environment.Environment = SConsEnvironment
451
452 def SetBuildSignatureType(type):
453     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
454                         "The SetBuildSignatureType() function has been deprecated;\n" +\
455                         "\tuse the TargetSignatures() function instead.")
456     SCons.Defaults.DefaultEnvironment().TargetSignatures(type)
457
458 def SetContentSignatureType(type):
459     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
460                         "The SetContentSignatureType() function has been deprecated;\n" +\
461                         "\tuse the SourceSignatures() function instead.")
462     SCons.Defaults.DefaultEnvironment().SourceSignatures(type)
463
464 class Options(SCons.Options.Options):
465     def __init__(self, files=None, args=arguments):
466         SCons.Options.Options.__init__(self, files, args)
467
468 def GetJobs():
469     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
470                         "The GetJobs() function has been deprecated;\n" +\
471                         "\tuse GetOption('num_jobs') instead.")
472
473     return GetOption('num_jobs')
474  
475 def SetJobs(num):
476     SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
477                         "The SetJobs() function has been deprecated;\n" +\
478                         "\tuse SetOption('num_jobs', num) instead.")
479     SetOption('num_jobs', num)
480
481
482 def Alias(name):
483     alias = SCons.Node.Alias.default_ans.lookup(name)
484     if alias is None:
485         alias = SCons.Node.Alias.default_ans.Alias(name)
486     return alias
487
488 #
489 _DefaultEnvironmentProxy = None
490
491 def get_DefaultEnvironmentProxy():
492     global _DefaultEnvironmentProxy
493     if not _DefaultEnvironmentProxy:
494         class EnvironmentProxy(SCons.Environment.Environment):
495             """A proxy subclass for an environment instance that overrides
496             the subst() and subst_list() methods so they don't actually
497             actually perform construction variable substitution.  This is
498             specifically intended to be the shim layer in between global
499             function calls (which don't want want construction variable
500             substitution) and the DefaultEnvironment() (which would
501             substitute variables if left to its own devices)."""
502             def __init__(self, subject):
503                 self.__dict__['__subject'] = subject
504             def __getattr__(self, name):
505                 return getattr(self.__dict__['__subject'], name)
506             def __setattr__(self, name, value):
507                 return setattr(self.__dict__['__subject'], name, value)
508             def subst(self, string, raw=0, target=None, source=None):
509                 return string
510             def subst_list(self, string, raw=0, target=None, source=None):
511                 return string
512         default_env = SCons.Defaults.DefaultEnvironment()
513         _DefaultEnvironmentProxy = EnvironmentProxy(default_env)
514     return _DefaultEnvironmentProxy
515
516 def BuildDefaultGlobals():
517     """
518     Create a dictionary containing all the default globals for 
519     SConstruct and SConscript files.
520     """
521
522     globals = {}
523     globals['Action']            = SCons.Action.Action
524     globals['Alias']             = Alias
525     globals['ARGUMENTS']         = arguments
526     globals['Builder']           = SCons.Builder.Builder
527     globals['Configure']         = SCons.SConf.SConf
528     globals['CScan']             = SCons.Defaults.CScan
529     globals['DefaultEnvironment'] = SCons.Defaults.DefaultEnvironment
530     globals['Environment']       = SCons.Environment.Environment
531     globals['GetCommandHandler'] = SCons.Action.GetCommandHandler
532     globals['Literal']           = SCons.Util.Literal
533     globals['Options']           = Options
534     globals['ParseConfig']       = SCons.Util.ParseConfig
535     globals['Platform']          = SCons.Platform.Platform
536     globals['Return']            = Return
537     globals['SConscriptChdir']   = SConscriptChdir
538     globals['Scanner']           = SCons.Scanner.Base
539     globals['SetCommandHandler'] = SCons.Action.SetCommandHandler
540     globals['Split']             = SCons.Util.Split
541     globals['Tool']              = SCons.Tool.Tool
542     globals['Value']             = SCons.Node.Python.Value
543     globals['WhereIs']           = SCons.Util.WhereIs
544
545     # Deprecated functions, leave this here for now.
546     globals['GetJobs']           = GetJobs
547     globals['SetBuildSignatureType'] = SetBuildSignatureType
548     globals['SetContentSignatureType'] = SetContentSignatureType
549     globals['SetJobs']           = SetJobs
550
551     class DefaultEnvironmentCall:
552         """A class that implements "global function" calls of
553         Environment methods by fetching the specified method from the
554         DefaultEnvironment's class.  Note that this uses an intermediate
555         proxy class instead of calling the DefaultEnvironment method
556         directly so that the proxy can override the subst() method and
557         thereby prevent expansion of construction variables (since from
558         the user's point of view this was called as a global function,
559         with no associated construction environment)."""
560         def __init__(self, method_name):
561             self.method_name = method_name
562         def __call__(self, *args, **kw):
563             proxy = get_DefaultEnvironmentProxy()
564             method = getattr(proxy.__class__, self.method_name)
565             return apply(method, (proxy,) + args, kw)
566
567     EnvironmentMethods = [
568         'AddPostAction',
569         'AddPreAction',
570         'AlwaysBuild',
571         'BuildDir',
572         'CacheDir',
573         'Clean',
574         'Command',
575         'Default',
576         'Depends',
577         'Dir',
578         'File',
579         'FindFile',
580         'GetBuildPath',
581         'Ignore',
582         'Install',
583         'InstallAs',
584         'Local',
585         'Precious',
586         'Repository',
587         'SConsignFile',
588         'SideEffect',
589         'SourceCode',
590         'SourceSignatures',
591         'TargetSignatures',
592     ]
593
594     SConsEnvironmentMethods = [
595         'EnsurePythonVersion',
596         'EnsureSConsVersion',
597         'Exit',
598         'Export',
599         'GetLaunchDir',
600         'GetOption',
601         'Help',
602         'Import',
603         'SConscript',
604         'SetOption',
605     ]
606
607     for name in EnvironmentMethods + SConsEnvironmentMethods:
608         globals[name] = DefaultEnvironmentCall(name)
609
610     return globals