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