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