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