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
25"""Builders and other things for the local site.
26
27Here's where we'll duplicate the functionality of autoconf until we
28move it into the installation procedure or use something like qmconf.
29
30The code that reads the registry to find MSVC components was borrowed
31from distutils.msvccompiler.
32"""
33
34import os
35import shutil
36import stat
37import sys
38import time
39
40import SCons.Action
41import SCons.Builder
42import SCons.CacheDir
43import SCons.Environment
44import SCons.PathList
45import SCons.Scanner.Dir
46import SCons.Subst
47import SCons.Tool
48
49# A placeholder for a default Environment (for fetching source files
50# from source code management systems and the like).  This must be
51# initialized later, after the top-level directory is set by the calling
52# interface.
53_default_env = None
54
55
56# Lazily instantiate the default environment so the overhead of creating
57# it doesn't apply when it's not needed.
58def _fetch_DefaultEnvironment(*args, **kw):
59    """Returns the already-created default construction environment."""
60    global _default_env
61    return _default_env
62
63
64def DefaultEnvironment(*args, **kw):
65    """
66    Initial public entry point for creating the default construction
67    Environment.
68
69    After creating the environment, we overwrite our name
70    (DefaultEnvironment) with the _fetch_DefaultEnvironment() function,
71    which more efficiently returns the initialized default construction
72    environment without checking for its existence.
73
74    (This function still exists with its _default_check because someone
75    else (*cough* Script/__init__.py *cough*) may keep a reference
76    to this function.  So we can't use the fully functional idiom of
77    having the name originally be a something that *only* creates the
78    construction environment and then overwrites the name.)
79    """
80    global _default_env
81    if not _default_env:
82        import SCons.Util
83        _default_env = SCons.Environment.Environment(*args, **kw)
84        _default_env.Decider('content')
85        global DefaultEnvironment
86        DefaultEnvironment = _fetch_DefaultEnvironment
87        _default_env._CacheDir_path = None
88    return _default_env
89
90
91# Emitters for setting the shared attribute on object files,
92# and an action for checking that all of the source files
93# going into a shared library are, in fact, shared.
94def StaticObjectEmitter(target, source, env):
95    for tgt in target:
96        tgt.attributes.shared = None
97    return target, source
98
99
100def SharedObjectEmitter(target, source, env):
101    for tgt in target:
102        tgt.attributes.shared = 1
103    return target, source
104
105
106def SharedFlagChecker(source, target, env):
107    same = env.subst('$STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME')
108    if same == '0' or same == '' or same == 'False':
109        for src in source:
110            try:
111                shared = src.attributes.shared
112            except AttributeError:
113                shared = None
114            if not shared:
115                raise SCons.Errors.UserError(
116                    "Source file: %s is static and is not compatible with shared target: %s" % (src, target[0]))
117
118
119SharedCheck = SCons.Action.Action(SharedFlagChecker, None)
120
121# Some people were using these variable name before we made
122# SourceFileScanner part of the public interface.  Don't break their
123# SConscript files until we've given them some fair warning and a
124# transition period.
125CScan = SCons.Tool.CScanner
126DScan = SCons.Tool.DScanner
127LaTeXScan = SCons.Tool.LaTeXScanner
128ObjSourceScan = SCons.Tool.SourceFileScanner
129ProgScan = SCons.Tool.ProgramScanner
130
131# These aren't really tool scanners, so they don't quite belong with
132# the rest of those in Tool/__init__.py, but I'm not sure where else
133# they should go.  Leave them here for now.
134
135DirScanner = SCons.Scanner.Dir.DirScanner()
136DirEntryScanner = SCons.Scanner.Dir.DirEntryScanner()
137
138# Actions for common languages.
139CAction = SCons.Action.Action("$CCCOM", "$CCCOMSTR")
140ShCAction = SCons.Action.Action("$SHCCCOM", "$SHCCCOMSTR")
141CXXAction = SCons.Action.Action("$CXXCOM", "$CXXCOMSTR")
142ShCXXAction = SCons.Action.Action("$SHCXXCOM", "$SHCXXCOMSTR")
143
144DAction = SCons.Action.Action("$DCOM", "$DCOMSTR")
145ShDAction = SCons.Action.Action("$SHDCOM", "$SHDCOMSTR")
146
147ASAction = SCons.Action.Action("$ASCOM", "$ASCOMSTR")
148ASPPAction = SCons.Action.Action("$ASPPCOM", "$ASPPCOMSTR")
149
150LinkAction = SCons.Action.Action("$LINKCOM", "$LINKCOMSTR")
151ShLinkAction = SCons.Action.Action("$SHLINKCOM", "$SHLINKCOMSTR")
152
153LdModuleLinkAction = SCons.Action.Action("$LDMODULECOM", "$LDMODULECOMSTR")
154
155# Common tasks that we allow users to perform in platform-independent
156# ways by creating ActionFactory instances.
157ActionFactory = SCons.Action.ActionFactory
158
159
160def get_paths_str(dest):
161    # If dest is a list, we need to manually call str() on each element
162    if SCons.Util.is_List(dest):
163        elem_strs = []
164        for element in dest:
165            elem_strs.append('"' + str(element) + '"')
166        return '[' + ', '.join(elem_strs) + ']'
167    else:
168        return '"' + str(dest) + '"'
169
170
171permission_dic = {
172    'u': {
173        'r': stat.S_IRUSR,
174        'w': stat.S_IWUSR,
175        'x': stat.S_IXUSR
176    },
177    'g': {
178        'r': stat.S_IRGRP,
179        'w': stat.S_IWGRP,
180        'x': stat.S_IXGRP
181    },
182    'o': {
183        'r': stat.S_IROTH,
184        'w': stat.S_IWOTH,
185        'x': stat.S_IXOTH
186    }
187}
188
189
190def chmod_func(dest, mode):
191    import SCons.Util
192    from string import digits
193    SCons.Node.FS.invalidate_node_memos(dest)
194    if not SCons.Util.is_List(dest):
195        dest = [dest]
196    if SCons.Util.is_String(mode) and 0 not in [i in digits for i in mode]:
197        mode = int(mode, 8)
198    if not SCons.Util.is_String(mode):
199        for element in dest:
200            os.chmod(str(element), mode)
201    else:
202        mode = str(mode)
203        for operation in mode.split(","):
204            if "=" in operation:
205                operator = "="
206            elif "+" in operation:
207                operator = "+"
208            elif "-" in operation:
209                operator = "-"
210            else:
211                raise SyntaxError("Could not find +, - or =")
212            operation_list = operation.split(operator)
213            if len(operation_list) != 2:
214                raise SyntaxError("More than one operator found")
215            user = operation_list[0].strip().replace("a", "ugo")
216            permission = operation_list[1].strip()
217            new_perm = 0
218            for u in user:
219                for p in permission:
220                    try:
221                        new_perm = new_perm | permission_dic[u][p]
222                    except KeyError:
223                        raise SyntaxError("Unrecognized user or permission format")
224            for element in dest:
225                curr_perm = os.stat(str(element)).st_mode
226                if operator == "=":
227                    os.chmod(str(element), new_perm)
228                elif operator == "+":
229                    os.chmod(str(element), curr_perm | new_perm)
230                elif operator == "-":
231                    os.chmod(str(element), curr_perm & ~new_perm)
232
233
234def chmod_strfunc(dest, mode):
235    import SCons.Util
236    if not SCons.Util.is_String(mode):
237        return 'Chmod(%s, 0%o)' % (get_paths_str(dest), mode)
238    else:
239        return 'Chmod(%s, "%s")' % (get_paths_str(dest), str(mode))
240
241
242Chmod = ActionFactory(chmod_func, chmod_strfunc)
243
244
245def copy_func(dest, src, symlinks=True):
246    """
247    If symlinks (is true), then a symbolic link will be
248    shallow copied and recreated as a symbolic link; otherwise, copying
249    a symbolic link will be equivalent to copying the symbolic link's
250    final target regardless of symbolic link depth.
251    """
252
253    dest = str(dest)
254    src = str(src)
255
256    SCons.Node.FS.invalidate_node_memos(dest)
257    if SCons.Util.is_List(src) and os.path.isdir(dest):
258        for file in src:
259            shutil.copy2(file, dest)
260        return 0
261    elif os.path.islink(src):
262        if symlinks:
263            return os.symlink(os.readlink(src), dest)
264        else:
265            return copy_func(dest, os.path.realpath(src))
266    elif os.path.isfile(src):
267        shutil.copy2(src, dest)
268        return 0
269    else:
270        shutil.copytree(src, dest, symlinks)
271        # copytree returns None in python2 and destination string in python3
272        # A error is raised in both cases, so we can just return 0 for success
273        return 0
274
275
276Copy = ActionFactory(
277    copy_func,
278    lambda dest, src, symlinks=True: 'Copy("%s", "%s")' % (dest, src)
279)
280
281
282def delete_func(dest, must_exist=0):
283    SCons.Node.FS.invalidate_node_memos(dest)
284    if not SCons.Util.is_List(dest):
285        dest = [dest]
286    for entry in dest:
287        entry = str(entry)
288        # os.path.exists returns False with broken links that exist
289        entry_exists = os.path.exists(entry) or os.path.islink(entry)
290        if not entry_exists and not must_exist:
291            continue
292        # os.path.isdir returns True when entry is a link to a dir
293        if os.path.isdir(entry) and not os.path.islink(entry):
294            shutil.rmtree(entry, True)
295            continue
296        os.unlink(entry)
297
298
299def delete_strfunc(dest, must_exist=0):
300    return 'Delete(%s)' % get_paths_str(dest)
301
302
303Delete = ActionFactory(delete_func, delete_strfunc)
304
305
306def mkdir_func(dest):
307    SCons.Node.FS.invalidate_node_memos(dest)
308    if not SCons.Util.is_List(dest):
309        dest = [dest]
310    for entry in dest:
311        os.makedirs(str(entry), exist_ok=True)
312
313
314Mkdir = ActionFactory(mkdir_func,
315                      lambda _dir: 'Mkdir(%s)' % get_paths_str(_dir))
316
317
318def move_func(dest, src):
319    SCons.Node.FS.invalidate_node_memos(dest)
320    SCons.Node.FS.invalidate_node_memos(src)
321    shutil.move(src, dest)
322
323
324Move = ActionFactory(move_func,
325                     lambda dest, src: 'Move("%s", "%s")' % (dest, src),
326                     convert=str)
327
328
329def touch_func(dest):
330    SCons.Node.FS.invalidate_node_memos(dest)
331    if not SCons.Util.is_List(dest):
332        dest = [dest]
333    for file in dest:
334        file = str(file)
335        mtime = int(time.time())
336        if os.path.exists(file):
337            atime = os.path.getatime(file)
338        else:
339            with open(file, 'w'):
340                atime = mtime
341        os.utime(file, (atime, mtime))
342
343
344Touch = ActionFactory(touch_func,
345                      lambda file: 'Touch(%s)' % get_paths_str(file))
346
347
348# Internal utility functions
349
350# pylint: disable-msg=too-many-arguments
351def _concat(prefix, items_iter, suffix, env, f=lambda x: x, target=None, source=None, affect_signature=True):
352    """
353    Creates a new list from 'items_iter' by first interpolating each element
354    in the list using the 'env' dictionary and then calling f on the
355    list, and finally calling _concat_ixes to concatenate 'prefix' and
356    'suffix' onto each element of the list.
357    """
358
359    if not items_iter:
360        return items_iter
361
362    l = f(SCons.PathList.PathList(items_iter).subst_path(env, target, source))
363    if l is not None:
364        items_iter = l
365
366    if not affect_signature:
367        value = ['$(']
368    else:
369        value = []
370    value += _concat_ixes(prefix, items_iter, suffix, env)
371
372    if not affect_signature:
373        value += ["$)"]
374
375    return value
376# pylint: enable-msg=too-many-arguments
377
378
379def _concat_ixes(prefix, items_iter, suffix, env):
380    """
381    Creates a new list from 'items_iter' by concatenating the 'prefix' and
382    'suffix' arguments onto each element of the list.  A trailing space
383    on 'prefix' or leading space on 'suffix' will cause them to be put
384    into separate list elements rather than being concatenated.
385    """
386
387    result = []
388
389    # ensure that prefix and suffix are strings
390    prefix = str(env.subst(prefix, SCons.Subst.SUBST_RAW))
391    suffix = str(env.subst(suffix, SCons.Subst.SUBST_RAW))
392
393    for x in SCons.Util.flatten(items_iter):
394        if isinstance(x, SCons.Node.FS.File):
395            result.append(x)
396            continue
397        x = str(x)
398        if x:
399
400            if prefix:
401                if prefix[-1] == ' ':
402                    result.append(prefix[:-1])
403                elif x[:len(prefix)] != prefix:
404                    x = prefix + x
405
406            result.append(x)
407
408            if suffix:
409                if suffix[0] == ' ':
410                    result.append(suffix[1:])
411                elif x[-len(suffix):] != suffix:
412                    result[-1] = result[-1] + suffix
413
414    return result
415
416
417def _stripixes(prefix, itms, suffix, stripprefixes, stripsuffixes, env, c=None):
418    """
419    This is a wrapper around _concat()/_concat_ixes() that checks for
420    the existence of prefixes or suffixes on list items and strips them
421    where it finds them.  This is used by tools (like the GNU linker)
422    that need to turn something like 'libfoo.a' into '-lfoo'.
423    """
424
425    if not itms:
426        return itms
427
428    if not callable(c):
429        env_c = env['_concat']
430        if env_c != _concat and callable(env_c):
431            # There's a custom _concat() method in the construction
432            # environment, and we've allowed people to set that in
433            # the past (see test/custom-concat.py), so preserve the
434            # backwards compatibility.
435            c = env_c
436        else:
437            c = _concat_ixes
438
439    stripprefixes = list(map(env.subst, SCons.Util.flatten(stripprefixes)))
440    stripsuffixes = list(map(env.subst, SCons.Util.flatten(stripsuffixes)))
441
442    stripped = []
443    for l in SCons.PathList.PathList(itms).subst_path(env, None, None):
444        if isinstance(l, SCons.Node.FS.File):
445            stripped.append(l)
446            continue
447
448        if not SCons.Util.is_String(l):
449            l = str(l)
450
451        for stripprefix in stripprefixes:
452            lsp = len(stripprefix)
453            if l[:lsp] == stripprefix:
454                l = l[lsp:]
455                # Do not strip more than one prefix
456                break
457
458        for stripsuffix in stripsuffixes:
459            lss = len(stripsuffix)
460            if l[-lss:] == stripsuffix:
461                l = l[:-lss]
462                # Do not strip more than one suffix
463                break
464
465        stripped.append(l)
466
467    return c(prefix, stripped, suffix, env)
468
469
470def processDefines(defs):
471    """process defines, resolving strings, lists, dictionaries, into a list of
472    strings
473    """
474    if SCons.Util.is_List(defs):
475        l = []
476        for d in defs:
477            if d is None:
478                continue
479            elif SCons.Util.is_List(d) or isinstance(d, tuple):
480                if len(d) >= 2:
481                    l.append(str(d[0]) + '=' + str(d[1]))
482                else:
483                    l.append(str(d[0]))
484            elif SCons.Util.is_Dict(d):
485                for macro, value in d.items():
486                    if value is not None:
487                        l.append(str(macro) + '=' + str(value))
488                    else:
489                        l.append(str(macro))
490            elif SCons.Util.is_String(d):
491                l.append(str(d))
492            else:
493                raise SCons.Errors.UserError("DEFINE %s is not a list, dict, string or None." % repr(d))
494    elif SCons.Util.is_Dict(defs):
495        # The items in a dictionary are stored in random order, but
496        # if the order of the command-line options changes from
497        # invocation to invocation, then the signature of the command
498        # line will change and we'll get random unnecessary rebuilds.
499        # Consequently, we have to sort the keys to ensure a
500        # consistent order...
501        l = []
502        for k, v in sorted(defs.items()):
503            if v is None:
504                l.append(str(k))
505            else:
506                l.append(str(k) + '=' + str(v))
507    else:
508        l = [str(defs)]
509    return l
510
511
512def _defines(prefix, defs, suffix, env, target, source, c=_concat_ixes):
513    """A wrapper around _concat_ixes that turns a list or string
514    into a list of C preprocessor command-line definitions.
515    """
516
517    return c(prefix, env.subst_list(processDefines(defs), target=target, source=source), suffix, env)
518
519
520class NullCmdGenerator:
521    """This is a callable class that can be used in place of other
522    command generators if you don't want them to do anything.
523
524    The __call__ method for this class simply returns the thing
525    you instantiated it with.
526
527    Example usage:
528    env["DO_NOTHING"] = NullCmdGenerator
529    env["LINKCOM"] = "${DO_NOTHING('$LINK $SOURCES $TARGET')}"
530    """
531
532    def __init__(self, cmd):
533        self.cmd = cmd
534
535    def __call__(self, target, source, env, for_signature=None):
536        return self.cmd
537
538
539class Variable_Method_Caller:
540    """A class for finding a construction variable on the stack and
541    calling one of its methods.
542
543    We use this to support "construction variables" in our string
544    eval()s that actually stand in for methods--specifically, use
545    of "RDirs" in call to _concat that should actually execute the
546    "TARGET.RDirs" method.  (We used to support this by creating a little
547    "build dictionary" that mapped RDirs to the method, but this got in
548    the way of Memoizing construction environments, because we had to
549    create new environment objects to hold the variables.)
550    """
551
552    def __init__(self, variable, method):
553        self.variable = variable
554        self.method = method
555
556    def __call__(self, *args, **kw):
557        try:
558            1 // 0
559        except ZeroDivisionError:
560            # Don't start iterating with the current stack-frame to
561            # prevent creating reference cycles (f_back is safe).
562            frame = sys.exc_info()[2].tb_frame.f_back
563        variable = self.variable
564        while frame:
565            if variable in frame.f_locals:
566                v = frame.f_locals[variable]
567                if v:
568                    method = getattr(v, self.method)
569                    return method(*args, **kw)
570            frame = frame.f_back
571        return None
572
573
574def __libversionflags(env, version_var, flags_var):
575    """
576    if version_var is not empty, returns env[flags_var], otherwise returns None
577    :param env:
578    :param version_var:
579    :param flags_var:
580    :return:
581    """
582    try:
583        if env.subst('$' + version_var):
584            return env[flags_var]
585    except KeyError:
586        pass
587    return None
588
589
590def __lib_either_version_flag(env, version_var1, version_var2, flags_var):
591    """
592    if $version_var1 or $version_var2 is not empty, returns env[flags_var], otherwise returns None
593    :param env:
594    :param version_var1:
595    :param version_var2:
596    :param flags_var:
597    :return:
598    """
599    try:
600        if env.subst('$' + version_var1) or env.subst('$' + version_var2):
601            return env[flags_var]
602    except KeyError:
603        pass
604    return None
605
606
607ConstructionEnvironment = {
608    'BUILDERS': {},
609    'SCANNERS': [SCons.Tool.SourceFileScanner],
610    'CONFIGUREDIR': '#/.sconf_temp',
611    'CONFIGURELOG': '#/config.log',
612    'CPPSUFFIXES': SCons.Tool.CSuffixes,
613    'DSUFFIXES': SCons.Tool.DSuffixes,
614    'ENV': {},
615    'IDLSUFFIXES': SCons.Tool.IDLSuffixes,
616    '_concat': _concat,
617    '_defines': _defines,
618    '_stripixes': _stripixes,
619    '_LIBFLAGS': '${_concat(LIBLINKPREFIX, LIBS, LIBLINKSUFFIX, __env__)}',
620
621    '_LIBDIRFLAGS': '${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}',
622    '_CPPINCFLAGS': '${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}',
623
624    '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, TARGET, SOURCE)}',
625
626    '__libversionflags': __libversionflags,
627    '__SHLIBVERSIONFLAGS': '${__libversionflags(__env__,"SHLIBVERSION","_SHLIBVERSIONFLAGS")}',
628    '__LDMODULEVERSIONFLAGS': '${__libversionflags(__env__,"LDMODULEVERSION","_LDMODULEVERSIONFLAGS")}',
629    '__DSHLIBVERSIONFLAGS': '${__libversionflags(__env__,"DSHLIBVERSION","_DSHLIBVERSIONFLAGS")}',
630    '__lib_either_version_flag': __lib_either_version_flag,
631
632    'TEMPFILE': NullCmdGenerator,
633    'TEMPFILEARGJOIN': ' ',
634    'TEMPFILEARGESCFUNC': SCons.Subst.quote_spaces,
635    'Dir': Variable_Method_Caller('TARGET', 'Dir'),
636    'Dirs': Variable_Method_Caller('TARGET', 'Dirs'),
637    'File': Variable_Method_Caller('TARGET', 'File'),
638    'RDirs': Variable_Method_Caller('TARGET', 'RDirs'),
639}
640
641# Local Variables:
642# tab-width:4
643# indent-tabs-mode:nil
644# End:
645# vim: set expandtab tabstop=4 shiftwidth=4:
646