http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / src / engine / SCons / Scanner / __init__.py
1 """SCons.Scanner
2
3 The Scanner package for the SCons software construction utility.
4
5 """
6
7 #
8 # __COPYRIGHT__
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__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
31
32 import re
33
34 import SCons.Node.FS
35 import SCons.Util
36
37
38 class _Null:
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
45 def 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
64 class FindPathDirs:
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
82 class Base:
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 = 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
269 class 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 = 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
300 class 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
313 class 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
385 class 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: