36a147dba69696441635328c610331eba8667986
[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.Options
43 import SCons.Platform
44 import SCons.SConf
45 import SCons.Script.Main
46 import SCons.Tool
47 import SCons.Util
48
49 import os
50 import os.path
51 import re
52 import string
53 import sys
54 import traceback
55 import types
56 import UserList
57
58 # The following variables used to live in this module.  Some
59 # SConscript files out there may have referred to them directly as
60 # SCons.Script.SConscript.*.  This is now supported by some special
61 # handling towards the bottom of the SConscript.__init__.py module.
62 #Arguments = {}
63 #ArgList = []
64 #BuildTargets = TargetList()
65 #CommandLineTargets = []
66 #DefaultTargets = []
67
68 class SConscriptReturn(Exception):
69     pass
70
71 launch_dir = os.path.abspath(os.curdir)
72
73 GlobalDict = None
74
75 # global exports set by Export():
76 global_exports = {}
77
78 # chdir flag
79 sconscript_chdir = 1
80
81 def get_calling_namespaces():
82     """Return the locals and globals for the function that called
83     into this module in the current call stack."""
84     try: 1/0
85     except ZeroDivisionError: frame = sys.exc_info()[2].tb_frame
86
87     # Find the first frame that *isn't* from this file.  This means
88     # that we expect all of the SCons frames that implement an Export()
89     # or SConscript() call to be in this file, so that we can identify
90     # the first non-Script.SConscript frame as the user's local calling
91     # environment, and the locals and globals dictionaries from that
92     # frame as the calling namespaces.  See the comment below preceding
93     # the DefaultEnvironmentCall block for even more explanation.
94     while frame.f_globals.get("__name__") == __name__:
95         frame = frame.f_back
96
97     return frame.f_locals, frame.f_globals
98
99
100 def compute_exports(exports):
101     """Compute a dictionary of exports given one of the parameters
102     to the Export() function or the exports argument to SConscript()."""
103
104     loc, glob = get_calling_namespaces()
105
106     retval = {}
107     try:
108         for export in exports:
109             if SCons.Util.is_Dict(export):
110                 retval.update(export)
111             else:
112                 try:
113                     retval[export] = loc[export]
114                 except KeyError:
115                     retval[export] = glob[export]
116     except KeyError, x:
117         raise SCons.Errors.UserError, "Export of non-existent variable '%s'"%x
118
119     return retval
120
121 class Frame:
122     """A frame on the SConstruct/SConscript call stack"""
123     def __init__(self, fs, exports, sconscript):
124         self.globals = BuildDefaultGlobals()
125         self.retval = None
126         self.prev_dir = fs.getcwd()
127         self.exports = compute_exports(exports)  # exports from the calling SConscript
128         # make sure the sconscript attr is a Node.
129         if isinstance(sconscript, SCons.Node.Node):
130             self.sconscript = sconscript
131         elif sconscript == '-':
132             self.sconscript = None
133         else:
134             self.sconscript = fs.File(str(sconscript))
135
136 # the SConstruct/SConscript call stack:
137 call_stack = []
138
139 # For documentation on the methods in this file, see the scons man-page
140
141 def Return(*vars, **kw):
142     retval = []
143     try:
144         for var in vars:
145             for v in string.split(var):
146                 retval.append(call_stack[-1].globals[v])
147     except KeyError, x:
148         raise SCons.Errors.UserError, "Return of non-existent variable '%s'"%x
149
150     if len(retval) == 1:
151         call_stack[-1].retval = retval[0]
152     else:
153         call_stack[-1].retval = tuple(retval)
154
155     stop = kw.get('stop', True)
156
157     if stop:
158         raise SConscriptReturn
159
160
161 stack_bottom = '% Stack boTTom %' # hard to define a variable w/this name :)
162
163 def _SConscript(fs, *files, **kw):
164     top = fs.Top
165     sd = fs.SConstruct_dir.rdir()
166     exports = kw.get('exports', [])
167
168     # evaluate each SConscript file
169     results = []
170     for fn in files:
171         call_stack.append(Frame(fs, exports, fn))
172         old_sys_path = sys.path
173         try:
174             SCons.Script.sconscript_reading = SCons.Script.sconscript_reading + 1
175             if fn == "-":
176                 exec sys.stdin in call_stack[-1].globals
177             else:
178                 if isinstance(fn, SCons.Node.Node):
179                     f = fn
180                 else:
181                     f = fs.File(str(fn))
182                 _file_ = None
183
184                 # Change directory to the top of the source
185                 # tree to make sure the os's cwd and the cwd of
186                 # fs match so we can open the SConscript.
187                 fs.chdir(top, change_os_dir=1)
188                 if f.rexists():
189                     _file_ = open(f.rfile().get_abspath(), "r")
190                 elif f.has_src_builder():
191                     # The SConscript file apparently exists in a source
192                     # code management system.  Build it, but then clear
193                     # the builder so that it doesn't get built *again*
194                     # during the actual build phase.
195                     f.build()
196                     f.built()
197                     f.builder_set(None)
198                     if f.exists():
199                         _file_ = open(f.get_abspath(), "r")
200                 if _file_:
201                     # Chdir to the SConscript directory.  Use a path
202                     # name relative to the SConstruct file so that if
203                     # we're using the -f option, we're essentially
204                     # creating a parallel SConscript directory structure
205                     # in our local directory tree.
206                     #
207                     # XXX This is broken for multiple-repository cases
208                     # where the SConstruct and SConscript files might be
209                     # in different Repositories.  For now, cross that
210                     # bridge when someone comes to it.
211                     try:
212                         src_dir = kw['src_dir']
213                     except KeyError:
214                         ldir = fs.Dir(f.dir.get_path(sd))
215                     else:
216                         ldir = fs.Dir(src_dir)
217                         if not ldir.is_under(f.dir):
218                             # They specified a source directory, but
219                             # it's above the SConscript directory.
220                             # Do the sensible thing and just use the
221                             # SConcript directory.
222                             ldir = fs.Dir(f.dir.get_path(sd))
223                     try:
224                         fs.chdir(ldir, change_os_dir=sconscript_chdir)
225                     except OSError:
226                         # There was no local directory, so we should be
227                         # able to chdir to the Repository directory.
228                         # Note that we do this directly, not through
229                         # fs.chdir(), because we still need to
230                         # interpret the stuff within the SConscript file
231                         # relative to where we are logically.
232                         fs.chdir(ldir, change_os_dir=0)
233                         # TODO Not sure how to handle src_dir here
234                         os.chdir(f.rfile().dir.get_abspath())
235
236                     # Append the SConscript directory to the beginning
237                     # of sys.path so Python modules in the SConscript
238                     # directory can be easily imported.
239                     sys.path = [ f.dir.get_abspath() ] + sys.path
240
241                     # This is the magic line that actually reads up
242                     # and executes the stuff in the SConscript file.
243                     # The locals for this frame contain the special
244                     # bottom-of-the-stack marker so that any
245                     # exceptions that occur when processing this
246                     # SConscript can base the printed frames at this
247                     # level and not show SCons internals as well.
248                     call_stack[-1].globals.update({stack_bottom:1})
249                     old_file = call_stack[-1].globals.get('__file__')
250                     try:
251                         del call_stack[-1].globals['__file__']
252                     except KeyError:
253                         pass
254                     try:
255                         try:
256                             exec _file_ in call_stack[-1].globals
257                         except SConscriptReturn:
258                             pass
259                     finally:
260                         if old_file is not None:
261                             call_stack[-1].globals.update({__file__:old_file})
262                 else:
263                     SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning,
264                              "Ignoring missing SConscript '%s'" % f.path)
265
266         finally:
267             SCons.Script.sconscript_reading = SCons.Script.sconscript_reading - 1
268             sys.path = old_sys_path
269             frame = call_stack.pop()
270             try:
271                 fs.chdir(frame.prev_dir, change_os_dir=sconscript_chdir)
272             except OSError:
273                 # There was no local directory, so chdir to the
274                 # Repository directory.  Like above, we do this
275                 # directly.
276                 fs.chdir(frame.prev_dir, change_os_dir=0)
277                 rdir = frame.prev_dir.rdir()
278                 rdir._create()  # Make sure there's a directory there.
279                 os.chdir(rdir.get_abspath())
280
281             results.append(frame.retval)
282
283     # if we only have one script, don't return a tuple
284     if len(results) == 1:
285         return results[0]
286     else:
287         return tuple(results)
288
289 def SConscript_exception(file=sys.stderr):
290     """Print an exception stack trace just for the SConscript file(s).
291     This will show users who have Python errors where the problem is,
292     without cluttering the output with all of the internal calls leading
293     up to where we exec the SConscript."""
294     exc_type, exc_value, exc_tb = sys.exc_info()
295     tb = exc_tb
296     while tb and not tb.tb_frame.f_locals.has_key(stack_bottom):
297         tb = tb.tb_next
298     if not tb:
299         # We did not find our exec statement, so this was actually a bug
300         # in SCons itself.  Show the whole stack.
301         tb = exc_tb
302     stack = traceback.extract_tb(tb)
303     try:
304         type = exc_type.__name__
305     except AttributeError:
306         type = str(exc_type)
307         if type[:11] == "exceptions.":
308             type = type[11:]
309     file.write('%s: %s:\n' % (type, exc_value))
310     for fname, line, func, text in stack:
311         file.write('  File "%s", line %d:\n' % (fname, line))
312         file.write('    %s\n' % text)
313
314 def annotate(node):
315     """Annotate a node with the stack frame describing the
316     SConscript file and line number that created it."""
317     tb = sys.exc_info()[2]
318     while tb and not tb.tb_frame.f_locals.has_key(stack_bottom):
319         tb = tb.tb_next
320     if not tb:
321         # We did not find any exec of an SConscript file: what?!
322         raise SCons.Errors.InternalError, "could not find SConscript stack frame"
323     node.creator = traceback.extract_stack(tb)[0]
324
325 # The following line would cause each Node to be annotated using the
326 # above function.  Unfortunately, this is a *huge* performance hit, so
327 # leave this disabled until we find a more efficient mechanism.
328 #SCons.Node.Annotate = annotate
329
330 class SConsEnvironment(SCons.Environment.Base):
331     """An Environment subclass that contains all of the methods that
332     are particular to the wrapper SCons interface and which aren't
333     (or shouldn't be) part of the build engine itself.
334
335     Note that not all of the methods of this class have corresponding
336     global functions, there are some private methods.
337     """
338
339     #
340     # Private methods of an SConsEnvironment.
341     #
342     def _exceeds_version(self, major, minor, v_major, v_minor):
343         """Return 1 if 'major' and 'minor' are greater than the version
344         in 'v_major' and 'v_minor', and 0 otherwise."""
345         return (major > v_major or (major == v_major and minor > v_minor))
346
347     def _get_major_minor_revision(self, version_string):
348         """Split a version string into major, minor and (optionally)
349         revision parts.
350
351         This is complicated by the fact that a version string can be
352         something like 3.2b1."""
353         version = string.split(string.split(version_string, ' ')[0], '.')
354         v_major = int(version[0])
355         v_minor = int(re.match('\d+', version[1]).group())
356         if len(version) >= 3:
357             v_revision = int(re.match('\d+', version[2]).group())
358         else:
359             v_revision = 0
360         return v_major, v_minor, v_revision
361
362     def _get_SConscript_filenames(self, ls, kw):
363         """
364         Convert the parameters passed to # SConscript() calls into a list
365         of files and export variables.  If the parameters are invalid,
366         throws SCons.Errors.UserError. Returns a tuple (l, e) where l
367         is a list of SConscript filenames and e is a list of exports.
368         """
369         exports = []
370
371         if len(ls) == 0:
372             try:
373                 dirs = kw["dirs"]
374             except KeyError:
375                 raise SCons.Errors.UserError, \
376                       "Invalid SConscript usage - no parameters"
377
378             if not SCons.Util.is_List(dirs):
379                 dirs = [ dirs ]
380             dirs = map(str, dirs)
381
382             name = kw.get('name', 'SConscript')
383
384             files = map(lambda n, name = name: os.path.join(n, name), dirs)
385
386         elif len(ls) == 1:
387
388             files = ls[0]
389
390         elif len(ls) == 2:
391
392             files   = ls[0]
393             exports = self.Split(ls[1])
394
395         else:
396
397             raise SCons.Errors.UserError, \
398                   "Invalid SConscript() usage - too many arguments"
399
400         if not SCons.Util.is_List(files):
401             files = [ files ]
402
403         if kw.get('exports'):
404             exports.extend(self.Split(kw['exports']))
405
406         variant_dir = kw.get('variant_dir') or kw.get('build_dir')
407         if variant_dir:
408             if len(files) != 1:
409                 raise SCons.Errors.UserError, \
410                     "Invalid SConscript() usage - can only specify one SConscript with a variant_dir"
411             duplicate = kw.get('duplicate', 1)
412             src_dir = kw.get('src_dir')
413             if not src_dir:
414                 src_dir, fname = os.path.split(str(files[0]))
415                 files = [os.path.join(str(variant_dir), fname)]
416             else:
417                 if not isinstance(src_dir, SCons.Node.Node):
418                     src_dir = self.fs.Dir(src_dir)
419                 fn = files[0]
420                 if not isinstance(fn, SCons.Node.Node):
421                     fn = self.fs.File(fn)
422                 if fn.is_under(src_dir):
423                     # Get path relative to the source directory.
424                     fname = fn.get_path(src_dir)
425                     files = [os.path.join(str(variant_dir), fname)]
426                 else:
427                     files = [fn.abspath]
428                 kw['src_dir'] = variant_dir
429             self.fs.VariantDir(variant_dir, src_dir, duplicate)
430
431         return (files, exports)
432
433     #
434     # Public methods of an SConsEnvironment.  These get
435     # entry points in the global name space so they can be called
436     # as global functions.
437     #
438
439     def Configure(self, *args, **kw):
440         if not SCons.Script.sconscript_reading:
441             raise SCons.Errors.UserError, "Calling Configure from Builders is not supported."
442         kw['_depth'] = kw.get('_depth', 0) + 1
443         return apply(SCons.Environment.Base.Configure, (self,)+args, kw)
444
445     def Default(self, *targets):
446         SCons.Script._Set_Default_Targets(self, targets)
447
448     def EnsureSConsVersion(self, major, minor, revision=0):
449         """Exit abnormally if the SCons version is not late enough."""
450         scons_ver = self._get_major_minor_revision(SCons.__version__)
451         if scons_ver < (major, minor, revision):
452             if revision:
453                 scons_ver_string = '%d.%d.%d' % (major, minor, revision)
454             else:
455                 scons_ver_string = '%d.%d' % (major, minor)
456             print "SCons %s or greater required, but you have SCons %s" % \
457                   (scons_ver_string, SCons.__version__)
458             sys.exit(2)
459
460     def EnsurePythonVersion(self, major, minor):
461         """Exit abnormally if the Python version is not late enough."""
462         try:
463             v_major, v_minor, v_micro, release, serial = sys.version_info
464             python_ver = (v_major, v_minor)
465         except AttributeError:
466             python_ver = self._get_major_minor_revision(sys.version)[:2]
467         if python_ver < (major, minor):
468             v = string.split(sys.version, " ", 1)[0]
469             print "Python %d.%d or greater required, but you have Python %s" %(major,minor,v)
470             sys.exit(2)
471
472     def Exit(self, value=0):
473         sys.exit(value)
474
475     def Export(self, *vars):
476         for var in vars:
477             global_exports.update(compute_exports(self.Split(var)))
478
479     def GetLaunchDir(self):
480         global launch_dir
481         return launch_dir
482
483     def GetOption(self, name):
484         name = self.subst(name)
485         return SCons.Script.Main.GetOption(name)
486
487     def Help(self, text):
488         text = self.subst(text, raw=1)
489         SCons.Script.HelpFunction(text)
490
491     def Import(self, *vars):
492         try:
493             frame = call_stack[-1]
494             globals = frame.globals
495             exports = frame.exports
496             for var in vars:
497                 var = self.Split(var)
498                 for v in var:
499                     if v == '*':
500                         globals.update(global_exports)
501                         globals.update(exports)
502                     else:
503                         if exports.has_key(v):
504                             globals[v] = exports[v]
505                         else:
506                             globals[v] = global_exports[v]
507         except KeyError,x:
508             raise SCons.Errors.UserError, "Import of non-existent variable '%s'"%x
509
510     def SConscript(self, *ls, **kw):
511         def subst_element(x, subst=self.subst):
512             if SCons.Util.is_List(x):
513                 x = map(subst, x)
514             else:
515                 x = subst(x)
516             return x
517         ls = map(subst_element, ls)
518         subst_kw = {}
519         for key, val in kw.items():
520             if SCons.Util.is_String(val):
521                 val = self.subst(val)
522             elif SCons.Util.is_List(val):
523                 result = []
524                 for v in val:
525                     if SCons.Util.is_String(v):
526                         v = self.subst(v)
527                     result.append(v)
528                 val = result
529             subst_kw[key] = val
530
531         files, exports = self._get_SConscript_filenames(ls, subst_kw)
532         subst_kw['exports'] = exports
533         return apply(_SConscript, [self.fs,] + files, subst_kw)
534
535     def SConscriptChdir(self, flag):
536         global sconscript_chdir
537         sconscript_chdir = flag
538
539     def SetOption(self, name, value):
540         name = self.subst(name)
541         SCons.Script.Main.SetOption(name, value)
542
543 #
544 #
545 #
546 SCons.Environment.Environment = SConsEnvironment
547
548 def Configure(*args, **kw):
549     if not SCons.Script.sconscript_reading:
550         raise SCons.Errors.UserError, "Calling Configure from Builders is not supported."
551     kw['_depth'] = 1
552     return apply(SCons.SConf.SConf, args, kw)
553
554 # It's very important that the DefaultEnvironmentCall() class stay in this
555 # file, with the get_calling_namespaces() function, the compute_exports()
556 # function, the Frame class and the SConsEnvironment.Export() method.
557 # These things make up the calling stack leading up to the actual global
558 # Export() or SConscript() call that the user issued.  We want to allow
559 # users to export local variables that they define, like so:
560 #
561 #       def func():
562 #           x = 1
563 #           Export('x')
564 #
565 # To support this, the get_calling_namespaces() function assumes that
566 # the *first* stack frame that's not from this file is the local frame
567 # for the Export() or SConscript() call.
568
569 _DefaultEnvironmentProxy = None
570
571 def get_DefaultEnvironmentProxy():
572     global _DefaultEnvironmentProxy
573     if not _DefaultEnvironmentProxy:
574         default_env = SCons.Defaults.DefaultEnvironment()
575         _DefaultEnvironmentProxy = SCons.Environment.NoSubstitutionProxy(default_env)
576     return _DefaultEnvironmentProxy
577
578 class DefaultEnvironmentCall:
579     """A class that implements "global function" calls of
580     Environment methods by fetching the specified method from the
581     DefaultEnvironment's class.  Note that this uses an intermediate
582     proxy class instead of calling the DefaultEnvironment method
583     directly so that the proxy can override the subst() method and
584     thereby prevent expansion of construction variables (since from
585     the user's point of view this was called as a global function,
586     with no associated construction environment)."""
587     def __init__(self, method_name, subst=0):
588         self.method_name = method_name
589         if subst:
590             self.factory = SCons.Defaults.DefaultEnvironment
591         else:
592             self.factory = get_DefaultEnvironmentProxy
593     def __call__(self, *args, **kw):
594         env = self.factory()
595         method = getattr(env, self.method_name)
596         return apply(method, args, kw)
597
598
599 def BuildDefaultGlobals():
600     """
601     Create a dictionary containing all the default globals for
602     SConstruct and SConscript files.
603     """
604
605     global GlobalDict
606     if GlobalDict is None:
607         GlobalDict = {}
608
609         import SCons.Script
610         d = SCons.Script.__dict__
611         def not_a_module(m, d=d, mtype=type(SCons.Script)):
612              return type(d[m]) != mtype
613         for m in filter(not_a_module, dir(SCons.Script)):
614              GlobalDict[m] = d[m]
615
616     return GlobalDict.copy()