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