1"""SCons.Scanner
2
3The Scanner package for the SCons software construction utility.
4
5"""
6
7#
8# Copyright (c) 2001 - 2016 The SCons Foundation
9#
10# Permission is hereby granted, free of charge, to any person obtaining
11# a copy of this software and associated documentation files (the
12# "Software"), to deal in the Software without restriction, including
13# without limitation the rights to use, copy, modify, merge, publish,
14# distribute, sublicense, and/or sell copies of the Software, and to
15# permit persons to whom the Software is furnished to do so, subject to
16# the following conditions:
17#
18# The above copyright notice and this permission notice shall be included
19# in all copies or substantial portions of the Software.
20#
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28#
29
30__revision__ = "src/engine/SCons/Scanner/__init__.py rel_2.5.0:3543:937e55cd78f7 2016/04/09 11:29:54 bdbaddog"
31
32import re
33
34import SCons.Node.FS
35import SCons.Util
36
37
38class _Null(object):
39    pass
40
41# This is used instead of None as a default argument value so None can be
42# used as an actual argument value.
43_null = _Null
44
45def Scanner(function, *args, **kw):
46    """
47    Public interface factory function for creating different types
48    of Scanners based on the different types of "functions" that may
49    be supplied.
50
51    TODO:  Deprecate this some day.  We've moved the functionality
52    inside the Base class and really don't need this factory function
53    any more.  It was, however, used by some of our Tool modules, so
54    the call probably ended up in various people's custom modules
55    patterned on SCons code.
56    """
57    if SCons.Util.is_Dict(function):
58        return Selector(function, *args, **kw)
59    else:
60        return Base(function, *args, **kw)
61
62
63
64class FindPathDirs(object):
65    """A class to bind a specific *PATH variable name to a function that
66    will return all of the *path directories."""
67    def __init__(self, variable):
68        self.variable = variable
69    def __call__(self, env, dir=None, target=None, source=None, argument=None):
70        import SCons.PathList
71        try:
72            path = env[self.variable]
73        except KeyError:
74            return ()
75
76        dir = dir or env.fs._cwd
77        path = SCons.PathList.PathList(path).subst_path(env, target, source)
78        return tuple(dir.Rfindalldirs(path))
79
80
81
82class Base(object):
83    """
84    The base class for dependency scanners.  This implements
85    straightforward, single-pass scanning of a single file.
86    """
87
88    def __init__(self,
89                 function,
90                 name = "NONE",
91                 argument = _null,
92                 skeys = _null,
93                 path_function = None,
94                 # Node.FS.Base so that, by default, it's okay for a
95                 # scanner to return a Dir, File or Entry.
96                 node_class = SCons.Node.FS.Base,
97                 node_factory = None,
98                 scan_check = None,
99                 recursive = None):
100        """
101        Construct a new scanner object given a scanner function.
102
103        'function' - a scanner function taking two or three
104        arguments and returning a list of strings.
105
106        'name' - a name for identifying this scanner object.
107
108        'argument' - an optional argument that, if specified, will be
109        passed to both the scanner function and the path_function.
110
111        'skeys' - an optional list argument that can be used to determine
112        which scanner should be used for a given Node. In the case of File
113        nodes, for example, the 'skeys' would be file suffixes.
114
115        'path_function' - a function that takes four or five arguments
116        (a construction environment, Node for the directory containing
117        the SConscript file that defined the primary target, list of
118        target nodes, list of source nodes, and optional argument for
119        this instance) and returns a tuple of the directories that can
120        be searched for implicit dependency files.  May also return a
121        callable() which is called with no args and returns the tuple
122        (supporting Bindable class).
123
124        'node_class' - the class of Nodes which this scan will return.
125        If node_class is None, then this scanner will not enforce any
126        Node conversion and will return the raw results from the
127        underlying scanner function.
128
129        'node_factory' - the factory function to be called to translate
130        the raw results returned by the scanner function into the
131        expected node_class objects.
132
133        'scan_check' - a function to be called to first check whether
134        this node really needs to be scanned.
135
136        'recursive' - specifies that this scanner should be invoked
137        recursively on all of the implicit dependencies it returns
138        (the canonical example being #include lines in C source files).
139        May be a callable, which will be called to filter the list
140        of nodes found to select a subset for recursive scanning
141        (the canonical example being only recursively scanning
142        subdirectories within a directory).
143
144        The scanner function's first argument will be a Node that should
145        be scanned for dependencies, the second argument will be an
146        Environment object, the third argument will be the tuple of paths
147        returned by the path_function, and the fourth argument will be
148        the value passed into 'argument', and the returned list should
149        contain the Nodes for all the direct dependencies of the file.
150
151        Examples:
152
153        s = Scanner(my_scanner_function)
154
155        s = Scanner(function = my_scanner_function)
156
157        s = Scanner(function = my_scanner_function, argument = 'foo')
158
159        """
160
161        # Note: this class could easily work with scanner functions that take
162        # something other than a filename as an argument (e.g. a database
163        # node) and a dependencies list that aren't file names. All that
164        # would need to be changed is the documentation.
165
166        self.function = function
167        self.path_function = path_function
168        self.name = name
169        self.argument = argument
170
171        if skeys is _null:
172            if SCons.Util.is_Dict(function):
173                skeys = list(function.keys())
174            else:
175                skeys = []
176        self.skeys = skeys
177
178        self.node_class = node_class
179        self.node_factory = node_factory
180        self.scan_check = scan_check
181        if callable(recursive):
182            self.recurse_nodes = recursive
183        elif recursive:
184            self.recurse_nodes = self._recurse_all_nodes
185        else:
186            self.recurse_nodes = self._recurse_no_nodes
187
188    def path(self, env, dir=None, target=None, source=None):
189        if not self.path_function:
190            return ()
191        if not self.argument is _null:
192            return self.path_function(env, dir, target, source, self.argument)
193        else:
194            return self.path_function(env, dir, target, source)
195
196    def __call__(self, node, env, path = ()):
197        """
198        This method scans a single object. 'node' is the node
199        that will be passed to the scanner function, and 'env' is the
200        environment that will be passed to the scanner function. A list of
201        direct dependency nodes for the specified node will be returned.
202        """
203        if self.scan_check and not self.scan_check(node, env):
204            return []
205
206        self = self.select(node)
207
208        if not self.argument is _null:
209            list = self.function(node, env, path, self.argument)
210        else:
211            list = self.function(node, env, path)
212
213        kw = {}
214        if hasattr(node, 'dir'):
215            kw['directory'] = node.dir
216        node_factory = env.get_factory(self.node_factory)
217        nodes = []
218        for l in list:
219            if self.node_class and not isinstance(l, self.node_class):
220                l = node_factory(l, **kw)
221            nodes.append(l)
222        return nodes
223
224    def __cmp__(self, other):
225        try:
226            return cmp(self.__dict__, other.__dict__)
227        except AttributeError:
228            # other probably doesn't have a __dict__
229            return cmp(self.__dict__, other)
230
231    def __hash__(self):
232        return id(self)
233
234    def __str__(self):
235        return self.name
236
237    def add_skey(self, skey):
238        """Add a skey to the list of skeys"""
239        self.skeys.append(skey)
240
241    def get_skeys(self, env=None):
242        if env and SCons.Util.is_String(self.skeys):
243            return env.subst_list(self.skeys)[0]
244        return self.skeys
245
246    def select(self, node):
247        if SCons.Util.is_Dict(self.function):
248            key = node.scanner_key()
249            try:
250                return self.function[key]
251            except KeyError:
252                return None
253        else:
254            return self
255
256    def _recurse_all_nodes(self, nodes):
257        return nodes
258
259    def _recurse_no_nodes(self, nodes):
260        return []
261
262    recurse_nodes = _recurse_no_nodes
263
264    def add_scanner(self, skey, scanner):
265        self.function[skey] = scanner
266        self.add_skey(skey)
267
268
269class Selector(Base):
270    """
271    A class for selecting a more specific scanner based on the
272    scanner_key() (suffix) for a specific Node.
273
274    TODO:  This functionality has been moved into the inner workings of
275    the Base class, and this class will be deprecated at some point.
276    (It was never exposed directly as part of the public interface,
277    although it is used by the Scanner() factory function that was
278    used by various Tool modules and therefore was likely a template
279    for custom modules that may be out there.)
280    """
281    def __init__(self, dict, *args, **kw):
282        Base.__init__(self, None, *args, **kw)
283        self.dict = dict
284        self.skeys = list(dict.keys())
285
286    def __call__(self, node, env, path = ()):
287        return self.select(node)(node, env, path)
288
289    def select(self, node):
290        try:
291            return self.dict[node.scanner_key()]
292        except KeyError:
293            return None
294
295    def add_scanner(self, skey, scanner):
296        self.dict[skey] = scanner
297        self.add_skey(skey)
298
299
300class Current(Base):
301    """
302    A class for scanning files that are source files (have no builder)
303    or are derived files and are current (which implies that they exist,
304    either locally or in a repository).
305    """
306
307    def __init__(self, *args, **kw):
308        def current_check(node, env):
309            return not node.has_builder() or node.is_up_to_date()
310        kw['scan_check'] = current_check
311        Base.__init__(self, *args, **kw)
312
313class Classic(Current):
314    """
315    A Scanner subclass to contain the common logic for classic CPP-style
316    include scanning, but which can be customized to use different
317    regular expressions to find the includes.
318
319    Note that in order for this to work "out of the box" (without
320    overriding the find_include() and sort_key() methods), the regular
321    expression passed to the constructor must return the name of the
322    include file in group 0.
323    """
324
325    def __init__(self, name, suffixes, path_variable, regex, *args, **kw):
326
327        self.cre = re.compile(regex, re.M)
328
329        def _scan(node, env, path=(), self=self):
330            node = node.rfile()
331            if not node.exists():
332                return []
333            return self.scan(node, path)
334
335        kw['function'] = _scan
336        kw['path_function'] = FindPathDirs(path_variable)
337        kw['recursive'] = 1
338        kw['skeys'] = suffixes
339        kw['name'] = name
340
341        Current.__init__(self, *args, **kw)
342
343    def find_include(self, include, source_dir, path):
344        n = SCons.Node.FS.find_file(include, (source_dir,) + tuple(path))
345        return n, include
346
347    def sort_key(self, include):
348        return SCons.Node.FS._my_normcase(include)
349
350    def find_include_names(self, node):
351        return self.cre.findall(node.get_text_contents())
352
353    def scan(self, node, path=()):
354
355        # cache the includes list in node so we only scan it once:
356        if node.includes is not None:
357            includes = node.includes
358        else:
359            includes = self.find_include_names (node)
360            # Intern the names of the include files. Saves some memory
361            # if the same header is included many times.
362            node.includes = list(map(SCons.Util.silent_intern, includes))
363
364        # This is a hand-coded DSU (decorate-sort-undecorate, or
365        # Schwartzian transform) pattern.  The sort key is the raw name
366        # of the file as specifed on the #include line (including the
367        # " or <, since that may affect what file is found), which lets
368        # us keep the sort order constant regardless of whether the file
369        # is actually found in a Repository or locally.
370        nodes = []
371        source_dir = node.get_dir()
372        if callable(path):
373            path = path()
374        for include in includes:
375            n, i = self.find_include(include, source_dir, path)
376
377            if n is None:
378                SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
379                                    "No dependency generated for file: %s (included from: %s) -- file not found" % (i, node))
380            else:
381                nodes.append((self.sort_key(include), n))
382
383        return [pair[1] for pair in sorted(nodes)]
384
385class ClassicCPP(Classic):
386    """
387    A Classic Scanner subclass which takes into account the type of
388    bracketing used to include the file, and uses classic CPP rules
389    for searching for the files based on the bracketing.
390
391    Note that in order for this to work, the regular expression passed
392    to the constructor must return the leading bracket in group 0, and
393    the contained filename in group 1.
394    """
395    def find_include(self, include, source_dir, path):
396        if include[0] == '"':
397            paths = (source_dir,) + tuple(path)
398        else:
399            paths = tuple(path) + (source_dir,)
400
401        n = SCons.Node.FS.find_file(include[1], paths)
402
403        i = SCons.Util.silent_intern(include[1])
404        return n, i
405
406    def sort_key(self, include):
407        return SCons.Node.FS._my_normcase(' '.join(include))
408
409# Local Variables:
410# tab-width:4
411# indent-tabs-mode:nil
412# End:
413# vim: set expandtab tabstop=4 shiftwidth=4:
414