02dcdbfc51e807428e8fcdc925b8aa399c80a665
[scons.git] / src / engine / SCons / Node / FS.py
1 """scons.Node.FS
2
3 File system nodes.
4
5 These Nodes represent the canonical external objects that people think
6 of when they think of building software: files and directories.
7
8 This holds a "default_fs" variable that should be initialized with an FS
9 that can be used by scripts or modules looking for the canonical default.
10
11 """
12
13 #
14 # __COPYRIGHT__
15 #
16 # Permission is hereby granted, free of charge, to any person obtaining
17 # a copy of this software and associated documentation files (the
18 # "Software"), to deal in the Software without restriction, including
19 # without limitation the rights to use, copy, modify, merge, publish,
20 # distribute, sublicense, and/or sell copies of the Software, and to
21 # permit persons to whom the Software is furnished to do so, subject to
22 # the following conditions:
23 #
24 # The above copyright notice and this permission notice shall be included
25 # in all copies or substantial portions of the Software.
26 #
27 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
28 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
29 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 #
35
36 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
37
38 import fnmatch
39 from itertools import izip
40 import os
41 import os.path
42 import re
43 import shutil
44 import stat
45 import string
46 import sys
47 import time
48 import cStringIO
49
50 import SCons.Action
51 from SCons.Debug import logInstanceCreation
52 import SCons.Errors
53 import SCons.Memoize
54 import SCons.Node
55 import SCons.Node.Alias
56 import SCons.Subst
57 import SCons.Util
58 import SCons.Warnings
59
60 from SCons.Debug import Trace
61
62 do_store_info = True
63
64 # The max_drift value:  by default, use a cached signature value for
65 # any file that's been untouched for more than two days.
66 default_max_drift = 2*24*60*60
67
68 #
69 # We stringify these file system Nodes a lot.  Turning a file system Node
70 # into a string is non-trivial, because the final string representation
71 # can depend on a lot of factors:  whether it's a derived target or not,
72 # whether it's linked to a repository or source directory, and whether
73 # there's duplication going on.  The normal technique for optimizing
74 # calculations like this is to memoize (cache) the string value, so you
75 # only have to do the calculation once.
76 #
77 # A number of the above factors, however, can be set after we've already
78 # been asked to return a string for a Node, because a Repository() or
79 # VariantDir() call or the like may not occur until later in SConscript
80 # files.  So this variable controls whether we bother trying to save
81 # string values for Nodes.  The wrapper interface can set this whenever
82 # they're done mucking with Repository and VariantDir and the other stuff,
83 # to let this module know it can start returning saved string values
84 # for Nodes.
85 #
86 Save_Strings = None
87
88 def save_strings(val):
89     global Save_Strings
90     Save_Strings = val
91
92 #
93 # Avoid unnecessary function calls by recording a Boolean value that
94 # tells us whether or not os.path.splitdrive() actually does anything
95 # on this system, and therefore whether we need to bother calling it
96 # when looking up path names in various methods below.
97
98
99 do_splitdrive = None
100
101 def initialize_do_splitdrive():
102     global do_splitdrive
103     drive, path = os.path.splitdrive('X:/foo')
104     do_splitdrive = not not drive
105
106 initialize_do_splitdrive()
107
108 #
109
110 needs_normpath_check = None
111
112 def initialize_normpath_check():
113     """
114     Initialize the normpath_check regular expression.
115
116     This function is used by the unit tests to re-initialize the pattern
117     when testing for behavior with different values of os.sep.
118     """
119     global needs_normpath_check
120     if os.sep == '/':
121         pattern = r'.*/|\.$|\.\.$'
122     else:
123         pattern = r'.*[/%s]|\.$|\.\.$' % re.escape(os.sep)
124     needs_normpath_check = re.compile(pattern)
125
126 initialize_normpath_check()
127
128 #
129 # SCons.Action objects for interacting with the outside world.
130 #
131 # The Node.FS methods in this module should use these actions to
132 # create and/or remove files and directories; they should *not* use
133 # os.{link,symlink,unlink,mkdir}(), etc., directly.
134 #
135 # Using these SCons.Action objects ensures that descriptions of these
136 # external activities are properly displayed, that the displays are
137 # suppressed when the -s (silent) option is used, and (most importantly)
138 # the actions are disabled when the the -n option is used, in which case
139 # there should be *no* changes to the external file system(s)...
140 #
141
142 if hasattr(os, 'link'):
143     def _hardlink_func(fs, src, dst):
144         # If the source is a symlink, we can't just hard-link to it
145         # because a relative symlink may point somewhere completely
146         # different.  We must disambiguate the symlink and then
147         # hard-link the final destination file.
148         while fs.islink(src):
149             link = fs.readlink(src)
150             if not os.path.isabs(link):
151                 src = link
152             else:
153                 src = os.path.join(os.path.dirname(src), link)
154         fs.link(src, dst)
155 else:
156     _hardlink_func = None
157
158 if hasattr(os, 'symlink'):
159     def _softlink_func(fs, src, dst):
160         fs.symlink(src, dst)
161 else:
162     _softlink_func = None
163
164 def _copy_func(fs, src, dest):
165     shutil.copy2(src, dest)
166     st = fs.stat(src)
167     fs.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
168
169
170 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
171                     'hard-copy', 'soft-copy', 'copy']
172
173 Link_Funcs = [] # contains the callables of the specified duplication style
174
175 def set_duplicate(duplicate):
176     # Fill in the Link_Funcs list according to the argument
177     # (discarding those not available on the platform).
178
179     # Set up the dictionary that maps the argument names to the
180     # underlying implementations.  We do this inside this function,
181     # not in the top-level module code, so that we can remap os.link
182     # and os.symlink for testing purposes.
183     link_dict = {
184         'hard' : _hardlink_func,
185         'soft' : _softlink_func,
186         'copy' : _copy_func
187     }
188
189     if not duplicate in Valid_Duplicates:
190         raise SCons.Errors.InternalError, ("The argument of set_duplicate "
191                                            "should be in Valid_Duplicates")
192     global Link_Funcs
193     Link_Funcs = []
194     for func in string.split(duplicate,'-'):
195         if link_dict[func]:
196             Link_Funcs.append(link_dict[func])
197
198 def LinkFunc(target, source, env):
199     # Relative paths cause problems with symbolic links, so
200     # we use absolute paths, which may be a problem for people
201     # who want to move their soft-linked src-trees around. Those
202     # people should use the 'hard-copy' mode, softlinks cannot be
203     # used for that; at least I have no idea how ...
204     src = source[0].abspath
205     dest = target[0].abspath
206     dir, file = os.path.split(dest)
207     if dir and not target[0].fs.isdir(dir):
208         os.makedirs(dir)
209     if not Link_Funcs:
210         # Set a default order of link functions.
211         set_duplicate('hard-soft-copy')
212     fs = source[0].fs
213     # Now link the files with the previously specified order.
214     for func in Link_Funcs:
215         try:
216             func(fs, src, dest)
217             break
218         except (IOError, OSError):
219             # An OSError indicates something happened like a permissions
220             # problem or an attempt to symlink across file-system
221             # boundaries.  An IOError indicates something like the file
222             # not existing.  In either case, keeping trying additional
223             # functions in the list and only raise an error if the last
224             # one failed.
225             if func == Link_Funcs[-1]:
226                 # exception of the last link method (copy) are fatal
227                 raise
228             else:
229                 pass
230     return 0
231
232 Link = SCons.Action.Action(LinkFunc, None)
233 def LocalString(target, source, env):
234     return 'Local copy of %s from %s' % (target[0], source[0])
235
236 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
237
238 def UnlinkFunc(target, source, env):
239     t = target[0]
240     t.fs.unlink(t.abspath)
241     return 0
242
243 Unlink = SCons.Action.Action(UnlinkFunc, None)
244
245 def MkdirFunc(target, source, env):
246     t = target[0]
247     if not t.exists():
248         t.fs.mkdir(t.abspath)
249     return 0
250
251 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
252
253 MkdirBuilder = None
254
255 def get_MkdirBuilder():
256     global MkdirBuilder
257     if MkdirBuilder is None:
258         import SCons.Builder
259         import SCons.Defaults
260         # "env" will get filled in by Executor.get_build_env()
261         # calling SCons.Defaults.DefaultEnvironment() when necessary.
262         MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
263                                              env = None,
264                                              explain = None,
265                                              is_explicit = None,
266                                              target_scanner = SCons.Defaults.DirEntryScanner,
267                                              name = "MkdirBuilder")
268     return MkdirBuilder
269
270 class _Null:
271     pass
272
273 _null = _Null()
274
275 DefaultSCCSBuilder = None
276 DefaultRCSBuilder = None
277
278 def get_DefaultSCCSBuilder():
279     global DefaultSCCSBuilder
280     if DefaultSCCSBuilder is None:
281         import SCons.Builder
282         # "env" will get filled in by Executor.get_build_env()
283         # calling SCons.Defaults.DefaultEnvironment() when necessary.
284         act = SCons.Action.Action('$SCCSCOM', '$SCCSCOMSTR')
285         DefaultSCCSBuilder = SCons.Builder.Builder(action = act,
286                                                    env = None,
287                                                    name = "DefaultSCCSBuilder")
288     return DefaultSCCSBuilder
289
290 def get_DefaultRCSBuilder():
291     global DefaultRCSBuilder
292     if DefaultRCSBuilder is None:
293         import SCons.Builder
294         # "env" will get filled in by Executor.get_build_env()
295         # calling SCons.Defaults.DefaultEnvironment() when necessary.
296         act = SCons.Action.Action('$RCS_COCOM', '$RCS_COCOMSTR')
297         DefaultRCSBuilder = SCons.Builder.Builder(action = act,
298                                                   env = None,
299                                                   name = "DefaultRCSBuilder")
300     return DefaultRCSBuilder
301
302 # Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
303 _is_cygwin = sys.platform == "cygwin"
304 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
305     def _my_normcase(x):
306         return x
307 else:
308     def _my_normcase(x):
309         return string.upper(x)
310
311
312
313 class DiskChecker:
314     def __init__(self, type, do, ignore):
315         self.type = type
316         self.do = do
317         self.ignore = ignore
318         self.set_do()
319     def set_do(self):
320         self.__call__ = self.do
321     def set_ignore(self):
322         self.__call__ = self.ignore
323     def set(self, list):
324         if self.type in list:
325             self.set_do()
326         else:
327             self.set_ignore()
328
329 def do_diskcheck_match(node, predicate, errorfmt):
330     result = predicate()
331     try:
332         # If calling the predicate() cached a None value from stat(),
333         # remove it so it doesn't interfere with later attempts to
334         # build this Node as we walk the DAG.  (This isn't a great way
335         # to do this, we're reaching into an interface that doesn't
336         # really belong to us, but it's all about performance, so
337         # for now we'll just document the dependency...)
338         if node._memo['stat'] is None:
339             del node._memo['stat']
340     except (AttributeError, KeyError):
341         pass
342     if result:
343         raise TypeError, errorfmt % node.abspath
344
345 def ignore_diskcheck_match(node, predicate, errorfmt):
346     pass
347
348 def do_diskcheck_rcs(node, name):
349     try:
350         rcs_dir = node.rcs_dir
351     except AttributeError:
352         if node.entry_exists_on_disk('RCS'):
353             rcs_dir = node.Dir('RCS')
354         else:
355             rcs_dir = None
356         node.rcs_dir = rcs_dir
357     if rcs_dir:
358         return rcs_dir.entry_exists_on_disk(name+',v')
359     return None
360
361 def ignore_diskcheck_rcs(node, name):
362     return None
363
364 def do_diskcheck_sccs(node, name):
365     try:
366         sccs_dir = node.sccs_dir
367     except AttributeError:
368         if node.entry_exists_on_disk('SCCS'):
369             sccs_dir = node.Dir('SCCS')
370         else:
371             sccs_dir = None
372         node.sccs_dir = sccs_dir
373     if sccs_dir:
374         return sccs_dir.entry_exists_on_disk('s.'+name)
375     return None
376
377 def ignore_diskcheck_sccs(node, name):
378     return None
379
380 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
381 diskcheck_rcs = DiskChecker('rcs', do_diskcheck_rcs, ignore_diskcheck_rcs)
382 diskcheck_sccs = DiskChecker('sccs', do_diskcheck_sccs, ignore_diskcheck_sccs)
383
384 diskcheckers = [
385     diskcheck_match,
386     diskcheck_rcs,
387     diskcheck_sccs,
388 ]
389
390 def set_diskcheck(list):
391     for dc in diskcheckers:
392         dc.set(list)
393
394 def diskcheck_types():
395     return map(lambda dc: dc.type, diskcheckers)
396
397
398
399 class EntryProxy(SCons.Util.Proxy):
400     def __get_abspath(self):
401         entry = self.get()
402         return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
403                                              entry.name + "_abspath")
404
405     def __get_filebase(self):
406         name = self.get().name
407         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
408                                              name + "_filebase")
409
410     def __get_suffix(self):
411         name = self.get().name
412         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
413                                              name + "_suffix")
414
415     def __get_file(self):
416         name = self.get().name
417         return SCons.Subst.SpecialAttrWrapper(name, name + "_file")
418
419     def __get_base_path(self):
420         """Return the file's directory and file name, with the
421         suffix stripped."""
422         entry = self.get()
423         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
424                                              entry.name + "_base")
425
426     def __get_posix_path(self):
427         """Return the path with / as the path separator,
428         regardless of platform."""
429         if os.sep == '/':
430             return self
431         else:
432             entry = self.get()
433             r = string.replace(entry.get_path(), os.sep, '/')
434             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
435
436     def __get_windows_path(self):
437         """Return the path with \ as the path separator,
438         regardless of platform."""
439         if os.sep == '\\':
440             return self
441         else:
442             entry = self.get()
443             r = string.replace(entry.get_path(), os.sep, '\\')
444             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
445
446     def __get_srcnode(self):
447         return EntryProxy(self.get().srcnode())
448
449     def __get_srcdir(self):
450         """Returns the directory containing the source node linked to this
451         node via VariantDir(), or the directory of this node if not linked."""
452         return EntryProxy(self.get().srcnode().dir)
453
454     def __get_rsrcnode(self):
455         return EntryProxy(self.get().srcnode().rfile())
456
457     def __get_rsrcdir(self):
458         """Returns the directory containing the source node linked to this
459         node via VariantDir(), or the directory of this node if not linked."""
460         return EntryProxy(self.get().srcnode().rfile().dir)
461
462     def __get_dir(self):
463         return EntryProxy(self.get().dir)
464
465     dictSpecialAttrs = { "base"     : __get_base_path,
466                          "posix"    : __get_posix_path,
467                          "windows"  : __get_windows_path,
468                          "win32"    : __get_windows_path,
469                          "srcpath"  : __get_srcnode,
470                          "srcdir"   : __get_srcdir,
471                          "dir"      : __get_dir,
472                          "abspath"  : __get_abspath,
473                          "filebase" : __get_filebase,
474                          "suffix"   : __get_suffix,
475                          "file"     : __get_file,
476                          "rsrcpath" : __get_rsrcnode,
477                          "rsrcdir"  : __get_rsrcdir,
478                        }
479
480     def __getattr__(self, name):
481         # This is how we implement the "special" attributes
482         # such as base, posix, srcdir, etc.
483         try:
484             attr_function = self.dictSpecialAttrs[name]
485         except KeyError:
486             try:
487                 attr = SCons.Util.Proxy.__getattr__(self, name)
488             except AttributeError:
489                 entry = self.get()
490                 classname = string.split(str(entry.__class__), '.')[-1]
491                 if classname[-2:] == "'>":
492                     # new-style classes report their name as:
493                     #   "<class 'something'>"
494                     # instead of the classic classes:
495                     #   "something"
496                     classname = classname[:-2]
497                 raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
498             return attr
499         else:
500             return attr_function(self)
501
502 class Base(SCons.Node.Node):
503     """A generic class for file system entries.  This class is for
504     when we don't know yet whether the entry being looked up is a file
505     or a directory.  Instances of this class can morph into either
506     Dir or File objects by a later, more precise lookup.
507
508     Note: this class does not define __cmp__ and __hash__ for
509     efficiency reasons.  SCons does a lot of comparing of
510     Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
511     as fast as possible, which means we want to use Python's built-in
512     object identity comparisons.
513     """
514
515     memoizer_counters = []
516
517     def __init__(self, name, directory, fs):
518         """Initialize a generic Node.FS.Base object.
519
520         Call the superclass initialization, take care of setting up
521         our relative and absolute paths, identify our parent
522         directory, and indicate that this node should use
523         signatures."""
524         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
525         SCons.Node.Node.__init__(self)
526
527         self.name = name
528         self.suffix = SCons.Util.splitext(name)[1]
529         self.fs = fs
530
531         assert directory, "A directory must be provided"
532
533         self.abspath = directory.entry_abspath(name)
534         self.labspath = directory.entry_labspath(name)
535         if directory.path == '.':
536             self.path = name
537         else:
538             self.path = directory.entry_path(name)
539         if directory.tpath == '.':
540             self.tpath = name
541         else:
542             self.tpath = directory.entry_tpath(name)
543         self.path_elements = directory.path_elements + [self]
544
545         self.dir = directory
546         self.cwd = None # will hold the SConscript directory for target nodes
547         self.duplicate = directory.duplicate
548
549     def str_for_display(self):
550         return '"' + self.__str__() + '"'
551
552     def must_be_same(self, klass):
553         """
554         This node, which already existed, is being looked up as the
555         specified klass.  Raise an exception if it isn't.
556         """
557         if self.__class__ is klass or klass is Entry:
558             return
559         raise TypeError, "Tried to lookup %s '%s' as a %s." %\
560               (self.__class__.__name__, self.path, klass.__name__)
561
562     def get_dir(self):
563         return self.dir
564
565     def get_suffix(self):
566         return self.suffix
567
568     def rfile(self):
569         return self
570
571     def __str__(self):
572         """A Node.FS.Base object's string representation is its path
573         name."""
574         global Save_Strings
575         if Save_Strings:
576             return self._save_str()
577         return self._get_str()
578
579     memoizer_counters.append(SCons.Memoize.CountValue('_save_str'))
580
581     def _save_str(self):
582         try:
583             return self._memo['_save_str']
584         except KeyError:
585             pass
586         result = self._get_str()
587         self._memo['_save_str'] = result
588         return result
589
590     def _get_str(self):
591         global Save_Strings
592         if self.duplicate or self.is_derived():
593             return self.get_path()
594         srcnode = self.srcnode()
595         if srcnode.stat() is None and not self.stat() is None:
596             result = self.get_path()
597         else:
598             result = srcnode.get_path()
599         if not Save_Strings:
600             # We're not at the point where we're saving the string string
601             # representations of FS Nodes (because we haven't finished
602             # reading the SConscript files and need to have str() return
603             # things relative to them).  That also means we can't yet
604             # cache values returned (or not returned) by stat(), since
605             # Python code in the SConscript files might still create
606             # or otherwise affect the on-disk file.  So get rid of the
607             # values that the underlying stat() method saved.
608             try: del self._memo['stat']
609             except KeyError: pass
610             if not self is srcnode:
611                 try: del srcnode._memo['stat']
612                 except KeyError: pass
613         return result
614
615     rstr = __str__
616
617     memoizer_counters.append(SCons.Memoize.CountValue('stat'))
618
619     def stat(self):
620         try: return self._memo['stat']
621         except KeyError: pass
622         try: result = self.fs.stat(self.abspath)
623         except os.error: result = None
624         self._memo['stat'] = result
625         return result
626
627     def exists(self):
628         return not self.stat() is None
629
630     def rexists(self):
631         return self.rfile().exists()
632
633     def getmtime(self):
634         st = self.stat()
635         if st: return st[stat.ST_MTIME]
636         else: return None
637
638     def getsize(self):
639         st = self.stat()
640         if st: return st[stat.ST_SIZE]
641         else: return None
642
643     def isdir(self):
644         st = self.stat()
645         return not st is None and stat.S_ISDIR(st[stat.ST_MODE])
646
647     def isfile(self):
648         st = self.stat()
649         return not st is None and stat.S_ISREG(st[stat.ST_MODE])
650
651     if hasattr(os, 'symlink'):
652         def islink(self):
653             try: st = self.fs.lstat(self.abspath)
654             except os.error: return 0
655             return stat.S_ISLNK(st[stat.ST_MODE])
656     else:
657         def islink(self):
658             return 0                    # no symlinks
659
660     def is_under(self, dir):
661         if self is dir:
662             return 1
663         else:
664             return self.dir.is_under(dir)
665
666     def set_local(self):
667         self._local = 1
668
669     def srcnode(self):
670         """If this node is in a build path, return the node
671         corresponding to its source file.  Otherwise, return
672         ourself.
673         """
674         srcdir_list = self.dir.srcdir_list()
675         if srcdir_list:
676             srcnode = srcdir_list[0].Entry(self.name)
677             srcnode.must_be_same(self.__class__)
678             return srcnode
679         return self
680
681     def get_path(self, dir=None):
682         """Return path relative to the current working directory of the
683         Node.FS.Base object that owns us."""
684         if not dir:
685             dir = self.fs.getcwd()
686         if self == dir:
687             return '.'
688         path_elems = self.path_elements
689         try: i = path_elems.index(dir)
690         except ValueError: pass
691         else: path_elems = path_elems[i+1:]
692         path_elems = map(lambda n: n.name, path_elems)
693         return string.join(path_elems, os.sep)
694
695     def set_src_builder(self, builder):
696         """Set the source code builder for this node."""
697         self.sbuilder = builder
698         if not self.has_builder():
699             self.builder_set(builder)
700
701     def src_builder(self):
702         """Fetch the source code builder for this node.
703
704         If there isn't one, we cache the source code builder specified
705         for the directory (which in turn will cache the value from its
706         parent directory, and so on up to the file system root).
707         """
708         try:
709             scb = self.sbuilder
710         except AttributeError:
711             scb = self.dir.src_builder()
712             self.sbuilder = scb
713         return scb
714
715     def get_abspath(self):
716         """Get the absolute path of the file."""
717         return self.abspath
718
719     def for_signature(self):
720         # Return just our name.  Even an absolute path would not work,
721         # because that can change thanks to symlinks or remapped network
722         # paths.
723         return self.name
724
725     def get_subst_proxy(self):
726         try:
727             return self._proxy
728         except AttributeError:
729             ret = EntryProxy(self)
730             self._proxy = ret
731             return ret
732
733     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
734         """
735
736         Generates a target entry that corresponds to this entry (usually
737         a source file) with the specified prefix and suffix.
738
739         Note that this method can be overridden dynamically for generated
740         files that need different behavior.  See Tool/swig.py for
741         an example.
742         """
743         return self.dir.Entry(prefix + splitext(self.name)[0] + suffix)
744
745     def _Rfindalldirs_key(self, pathlist):
746         return pathlist
747
748     memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key))
749
750     def Rfindalldirs(self, pathlist):
751         """
752         Return all of the directories for a given path list, including
753         corresponding "backing" directories in any repositories.
754
755         The Node lookups are relative to this Node (typically a
756         directory), so memoizing result saves cycles from looking
757         up the same path for each target in a given directory.
758         """
759         try:
760             memo_dict = self._memo['Rfindalldirs']
761         except KeyError:
762             memo_dict = {}
763             self._memo['Rfindalldirs'] = memo_dict
764         else:
765             try:
766                 return memo_dict[pathlist]
767             except KeyError:
768                 pass
769
770         create_dir_relative_to_self = self.Dir
771         result = []
772         for path in pathlist:
773             if isinstance(path, SCons.Node.Node):
774                 result.append(path)
775             else:
776                 dir = create_dir_relative_to_self(path)
777                 result.extend(dir.get_all_rdirs())
778
779         memo_dict[pathlist] = result
780
781         return result
782
783     def RDirs(self, pathlist):
784         """Search for a list of directories in the Repository list."""
785         cwd = self.cwd or self.fs._cwd
786         return cwd.Rfindalldirs(pathlist)
787
788     memoizer_counters.append(SCons.Memoize.CountValue('rentry'))
789
790     def rentry(self):
791         try:
792             return self._memo['rentry']
793         except KeyError:
794             pass
795         result = self
796         if not self.exists():
797             norm_name = _my_normcase(self.name)
798             for dir in self.dir.get_all_rdirs():
799                 try:
800                     node = dir.entries[norm_name]
801                 except KeyError:
802                     if dir.entry_exists_on_disk(self.name):
803                         result = dir.Entry(self.name)
804                         break
805         self._memo['rentry'] = result
806         return result
807
808     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
809         return []
810
811 class Entry(Base):
812     """This is the class for generic Node.FS entries--that is, things
813     that could be a File or a Dir, but we're just not sure yet.
814     Consequently, the methods in this class really exist just to
815     transform their associated object into the right class when the
816     time comes, and then call the same-named method in the transformed
817     class."""
818
819     def diskcheck_match(self):
820         pass
821
822     def disambiguate(self, must_exist=None):
823         """
824         """
825         if self.isdir():
826             self.__class__ = Dir
827             self._morph()
828         elif self.isfile():
829             self.__class__ = File
830             self._morph()
831             self.clear()
832         else:
833             # There was nothing on-disk at this location, so look in
834             # the src directory.
835             #
836             # We can't just use self.srcnode() straight away because
837             # that would create an actual Node for this file in the src
838             # directory, and there might not be one.  Instead, use the
839             # dir_on_disk() method to see if there's something on-disk
840             # with that name, in which case we can go ahead and call
841             # self.srcnode() to create the right type of entry.
842             srcdir = self.dir.srcnode()
843             if srcdir != self.dir and \
844                srcdir.entry_exists_on_disk(self.name) and \
845                self.srcnode().isdir():
846                 self.__class__ = Dir
847                 self._morph()
848             elif must_exist:
849                 msg = "No such file or directory: '%s'" % self.abspath
850                 raise SCons.Errors.UserError, msg
851             else:
852                 self.__class__ = File
853                 self._morph()
854                 self.clear()
855         return self
856
857     def rfile(self):
858         """We're a generic Entry, but the caller is actually looking for
859         a File at this point, so morph into one."""
860         self.__class__ = File
861         self._morph()
862         self.clear()
863         return File.rfile(self)
864
865     def scanner_key(self):
866         return self.get_suffix()
867
868     def get_contents(self):
869         """Fetch the contents of the entry.
870
871         Since this should return the real contents from the file
872         system, we check to see into what sort of subclass we should
873         morph this Entry."""
874         try:
875             self = self.disambiguate(must_exist=1)
876         except SCons.Errors.UserError:
877             # There was nothing on disk with which to disambiguate
878             # this entry.  Leave it as an Entry, but return a null
879             # string so calls to get_contents() in emitters and the
880             # like (e.g. in qt.py) don't have to disambiguate by hand
881             # or catch the exception.
882             return ''
883         else:
884             return self.get_contents()
885
886     def must_be_same(self, klass):
887         """Called to make sure a Node is a Dir.  Since we're an
888         Entry, we can morph into one."""
889         if not self.__class__ is klass:
890             self.__class__ = klass
891             self._morph()
892             self.clear()
893
894     # The following methods can get called before the Taskmaster has
895     # had a chance to call disambiguate() directly to see if this Entry
896     # should really be a Dir or a File.  We therefore use these to call
897     # disambiguate() transparently (from our caller's point of view).
898     #
899     # Right now, this minimal set of methods has been derived by just
900     # looking at some of the methods that will obviously be called early
901     # in any of the various Taskmasters' calling sequences, and then
902     # empirically figuring out which additional methods are necessary
903     # to make various tests pass.
904
905     def exists(self):
906         """Return if the Entry exists.  Check the file system to see
907         what we should turn into first.  Assume a file if there's no
908         directory."""
909         return self.disambiguate().exists()
910
911     def rel_path(self, other):
912         d = self.disambiguate()
913         if d.__class__ == Entry:
914             raise "rel_path() could not disambiguate File/Dir"
915         return d.rel_path(other)
916
917     def new_ninfo(self):
918         return self.disambiguate().new_ninfo()
919
920     def changed_since_last_build(self, target, prev_ni):
921         return self.disambiguate().changed_since_last_build(target, prev_ni)
922
923     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
924         return self.disambiguate()._glob1(pattern, ondisk, source, strings)
925
926 # This is for later so we can differentiate between Entry the class and Entry
927 # the method of the FS class.
928 _classEntry = Entry
929
930
931 class LocalFS:
932
933     if SCons.Memoize.use_memoizer:
934         __metaclass__ = SCons.Memoize.Memoized_Metaclass
935
936     # This class implements an abstraction layer for operations involving
937     # a local file system.  Essentially, this wraps any function in
938     # the os, os.path or shutil modules that we use to actually go do
939     # anything with or to the local file system.
940     #
941     # Note that there's a very good chance we'll refactor this part of
942     # the architecture in some way as we really implement the interface(s)
943     # for remote file system Nodes.  For example, the right architecture
944     # might be to have this be a subclass instead of a base class.
945     # Nevertheless, we're using this as a first step in that direction.
946     #
947     # We're not using chdir() yet because the calling subclass method
948     # needs to use os.chdir() directly to avoid recursion.  Will we
949     # really need this one?
950     #def chdir(self, path):
951     #    return os.chdir(path)
952     def chmod(self, path, mode):
953         return os.chmod(path, mode)
954     def copy(self, src, dst):
955         return shutil.copy(src, dst)
956     def copy2(self, src, dst):
957         return shutil.copy2(src, dst)
958     def exists(self, path):
959         return os.path.exists(path)
960     def getmtime(self, path):
961         return os.path.getmtime(path)
962     def getsize(self, path):
963         return os.path.getsize(path)
964     def isdir(self, path):
965         return os.path.isdir(path)
966     def isfile(self, path):
967         return os.path.isfile(path)
968     def link(self, src, dst):
969         return os.link(src, dst)
970     def lstat(self, path):
971         return os.lstat(path)
972     def listdir(self, path):
973         return os.listdir(path)
974     def makedirs(self, path):
975         return os.makedirs(path)
976     def mkdir(self, path):
977         return os.mkdir(path)
978     def rename(self, old, new):
979         return os.rename(old, new)
980     def stat(self, path):
981         return os.stat(path)
982     def symlink(self, src, dst):
983         return os.symlink(src, dst)
984     def open(self, path):
985         return open(path)
986     def unlink(self, path):
987         return os.unlink(path)
988
989     if hasattr(os, 'symlink'):
990         def islink(self, path):
991             return os.path.islink(path)
992     else:
993         def islink(self, path):
994             return 0                    # no symlinks
995
996     if hasattr(os, 'readlink'):
997         def readlink(self, file):
998             return os.readlink(file)
999     else:
1000         def readlink(self, file):
1001             return ''
1002
1003
1004 #class RemoteFS:
1005 #    # Skeleton for the obvious methods we might need from the
1006 #    # abstraction layer for a remote filesystem.
1007 #    def upload(self, local_src, remote_dst):
1008 #        pass
1009 #    def download(self, remote_src, local_dst):
1010 #        pass
1011
1012
1013 class FS(LocalFS):
1014
1015     memoizer_counters = []
1016
1017     def __init__(self, path = None):
1018         """Initialize the Node.FS subsystem.
1019
1020         The supplied path is the top of the source tree, where we
1021         expect to find the top-level build file.  If no path is
1022         supplied, the current directory is the default.
1023
1024         The path argument must be a valid absolute path.
1025         """
1026         if __debug__: logInstanceCreation(self, 'Node.FS')
1027
1028         self._memo = {}
1029
1030         self.Root = {}
1031         self.SConstruct_dir = None
1032         self.max_drift = default_max_drift
1033
1034         self.Top = None
1035         if path is None:
1036             self.pathTop = os.getcwd()
1037         else:
1038             self.pathTop = path
1039         self.defaultDrive = _my_normcase(os.path.splitdrive(self.pathTop)[0])
1040
1041         self.Top = self.Dir(self.pathTop)
1042         self.Top.path = '.'
1043         self.Top.tpath = '.'
1044         self._cwd = self.Top
1045
1046         DirNodeInfo.fs = self
1047         FileNodeInfo.fs = self
1048     
1049     def set_SConstruct_dir(self, dir):
1050         self.SConstruct_dir = dir
1051
1052     def get_max_drift(self):
1053         return self.max_drift
1054
1055     def set_max_drift(self, max_drift):
1056         self.max_drift = max_drift
1057
1058     def getcwd(self):
1059         return self._cwd
1060
1061     def chdir(self, dir, change_os_dir=0):
1062         """Change the current working directory for lookups.
1063         If change_os_dir is true, we will also change the "real" cwd
1064         to match.
1065         """
1066         curr=self._cwd
1067         try:
1068             if not dir is None:
1069                 self._cwd = dir
1070                 if change_os_dir:
1071                     os.chdir(dir.abspath)
1072         except OSError:
1073             self._cwd = curr
1074             raise
1075
1076     def get_root(self, drive):
1077         """
1078         Returns the root directory for the specified drive, creating
1079         it if necessary.
1080         """
1081         drive = _my_normcase(drive)
1082         try:
1083             return self.Root[drive]
1084         except KeyError:
1085             root = RootDir(drive, self)
1086             self.Root[drive] = root
1087             if not drive:
1088                 self.Root[self.defaultDrive] = root
1089             elif drive == self.defaultDrive:
1090                 self.Root[''] = root
1091             return root
1092
1093     def _lookup(self, p, directory, fsclass, create=1):
1094         """
1095         The generic entry point for Node lookup with user-supplied data.
1096
1097         This translates arbitrary input into a canonical Node.FS object
1098         of the specified fsclass.  The general approach for strings is
1099         to turn it into a fully normalized absolute path and then call
1100         the root directory's lookup_abs() method for the heavy lifting.
1101
1102         If the path name begins with '#', it is unconditionally
1103         interpreted relative to the top-level directory of this FS.  '#'
1104         is treated as a synonym for the top-level SConstruct directory,
1105         much like '~' is treated as a synonym for the user's home
1106         directory in a UNIX shell.  So both '#foo' and '#/foo' refer
1107         to the 'foo' subdirectory underneath the top-level SConstruct
1108         directory.
1109
1110         If the path name is relative, then the path is looked up relative
1111         to the specified directory, or the current directory (self._cwd,
1112         typically the SConscript directory) if the specified directory
1113         is None.
1114         """
1115         if isinstance(p, Base):
1116             # It's already a Node.FS object.  Make sure it's the right
1117             # class and return.
1118             p.must_be_same(fsclass)
1119             return p
1120         # str(p) in case it's something like a proxy object
1121         p = str(p)
1122
1123         initial_hash = (p[0:1] == '#')
1124         if initial_hash:
1125             # There was an initial '#', so we strip it and override
1126             # whatever directory they may have specified with the
1127             # top-level SConstruct directory.
1128             p = p[1:]
1129             directory = self.Top
1130
1131         if directory and not isinstance(directory, Dir):
1132             directory = self.Dir(directory)
1133
1134         if do_splitdrive:
1135             drive, p = os.path.splitdrive(p)
1136         else:
1137             drive = ''
1138         if drive and not p:
1139             # This causes a naked drive letter to be treated as a synonym
1140             # for the root directory on that drive.
1141             p = os.sep
1142         absolute = os.path.isabs(p)
1143
1144         needs_normpath = needs_normpath_check.match(p)
1145
1146         if initial_hash or not absolute:
1147             # This is a relative lookup, either to the top-level
1148             # SConstruct directory (because of the initial '#') or to
1149             # the current directory (the path name is not absolute).
1150             # Add the string to the appropriate directory lookup path,
1151             # after which the whole thing gets normalized.
1152             if not directory:
1153                 directory = self._cwd
1154             if p:
1155                 p = directory.labspath + '/' + p
1156             else:
1157                 p = directory.labspath
1158
1159         if needs_normpath:
1160             p = os.path.normpath(p)
1161
1162         if drive or absolute:
1163             root = self.get_root(drive)
1164         else:
1165             if not directory:
1166                 directory = self._cwd
1167             root = directory.root
1168
1169         if os.sep != '/':
1170             p = string.replace(p, os.sep, '/')
1171         return root._lookup_abs(p, fsclass, create)
1172
1173     def Entry(self, name, directory = None, create = 1):
1174         """Lookup or create a generic Entry node with the specified name.
1175         If the name is a relative path (begins with ./, ../, or a file
1176         name), then it is looked up relative to the supplied directory
1177         node, or to the top level directory of the FS (supplied at
1178         construction time) if no directory is supplied.
1179         """
1180         return self._lookup(name, directory, Entry, create)
1181
1182     def File(self, name, directory = None, create = 1):
1183         """Lookup or create a File node with the specified name.  If
1184         the name is a relative path (begins with ./, ../, or a file name),
1185         then it is looked up relative to the supplied directory node,
1186         or to the top level directory of the FS (supplied at construction
1187         time) if no directory is supplied.
1188
1189         This method will raise TypeError if a directory is found at the
1190         specified path.
1191         """
1192         return self._lookup(name, directory, File, create)
1193
1194     def Dir(self, name, directory = None, create = True):
1195         """Lookup or create a Dir node with the specified name.  If
1196         the name is a relative path (begins with ./, ../, or a file name),
1197         then it is looked up relative to the supplied directory node,
1198         or to the top level directory of the FS (supplied at construction
1199         time) if no directory is supplied.
1200
1201         This method will raise TypeError if a normal file is found at the
1202         specified path.
1203         """
1204         return self._lookup(name, directory, Dir, create)
1205
1206     def VariantDir(self, variant_dir, src_dir, duplicate=1):
1207         """Link the supplied variant directory to the source directory
1208         for purposes of building files."""
1209
1210         if not isinstance(src_dir, SCons.Node.Node):
1211             src_dir = self.Dir(src_dir)
1212         if not isinstance(variant_dir, SCons.Node.Node):
1213             variant_dir = self.Dir(variant_dir)
1214         if src_dir.is_under(variant_dir):
1215             raise SCons.Errors.UserError, "Source directory cannot be under variant directory."
1216         if variant_dir.srcdir:
1217             if variant_dir.srcdir == src_dir:
1218                 return # We already did this.
1219             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir)
1220         variant_dir.link(src_dir, duplicate)
1221
1222     def Repository(self, *dirs):
1223         """Specify Repository directories to search."""
1224         for d in dirs:
1225             if not isinstance(d, SCons.Node.Node):
1226                 d = self.Dir(d)
1227             self.Top.addRepository(d)
1228
1229     def variant_dir_target_climb(self, orig, dir, tail):
1230         """Create targets in corresponding variant directories
1231
1232         Climb the directory tree, and look up path names
1233         relative to any linked variant directories we find.
1234
1235         Even though this loops and walks up the tree, we don't memoize
1236         the return value because this is really only used to process
1237         the command-line targets.
1238         """
1239         targets = []
1240         message = None
1241         fmt = "building associated VariantDir targets: %s"
1242         start_dir = dir
1243         while dir:
1244             for bd in dir.variant_dirs:
1245                 if start_dir.is_under(bd):
1246                     # If already in the build-dir location, don't reflect
1247                     return [orig], fmt % str(orig)
1248                 p = apply(os.path.join, [bd.path] + tail)
1249                 targets.append(self.Entry(p))
1250             tail = [dir.name] + tail
1251             dir = dir.up()
1252         if targets:
1253             message = fmt % string.join(map(str, targets))
1254         return targets, message
1255
1256     def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
1257         """
1258         Globs
1259
1260         This is mainly a shim layer 
1261         """
1262         if cwd is None:
1263             cwd = self.getcwd()
1264         return cwd.glob(pathname, ondisk, source, strings)
1265
1266 class DirNodeInfo(SCons.Node.NodeInfoBase):
1267     # This should get reset by the FS initialization.
1268     current_version_id = 1
1269
1270     fs = None
1271
1272     def str_to_node(self, s):
1273         top = self.fs.Top
1274         root = top.root
1275         if do_splitdrive:
1276             drive, s = os.path.splitdrive(s)
1277             if drive:
1278                 root = self.fs.get_root(drive)
1279         if not os.path.isabs(s):
1280             s = top.labspath + '/' + s
1281         return root._lookup_abs(s, Entry)
1282
1283 class DirBuildInfo(SCons.Node.BuildInfoBase):
1284     current_version_id = 1
1285
1286 glob_magic_check = re.compile('[*?[]')
1287
1288 def has_glob_magic(s):
1289     return glob_magic_check.search(s) is not None
1290
1291 class Dir(Base):
1292     """A class for directories in a file system.
1293     """
1294
1295     memoizer_counters = []
1296
1297     NodeInfo = DirNodeInfo
1298     BuildInfo = DirBuildInfo
1299
1300     def __init__(self, name, directory, fs):
1301         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1302         Base.__init__(self, name, directory, fs)
1303         self._morph()
1304
1305     def _morph(self):
1306         """Turn a file system Node (either a freshly initialized directory
1307         object or a separate Entry object) into a proper directory object.
1308
1309         Set up this directory's entries and hook it into the file
1310         system tree.  Specify that directories (this Node) don't use
1311         signatures for calculating whether they're current.
1312         """
1313
1314         self.repositories = []
1315         self.srcdir = None
1316
1317         self.entries = {}
1318         self.entries['.'] = self
1319         self.entries['..'] = self.dir
1320         self.cwd = self
1321         self.searched = 0
1322         self._sconsign = None
1323         self.variant_dirs = []
1324         self.root = self.dir.root
1325
1326         # Don't just reset the executor, replace its action list,
1327         # because it might have some pre-or post-actions that need to
1328         # be preserved.
1329         self.builder = get_MkdirBuilder()
1330         self.get_executor().set_action_list(self.builder.action)
1331
1332     def diskcheck_match(self):
1333         diskcheck_match(self, self.isfile,
1334                         "File %s found where directory expected.")
1335
1336     def __clearRepositoryCache(self, duplicate=None):
1337         """Called when we change the repository(ies) for a directory.
1338         This clears any cached information that is invalidated by changing
1339         the repository."""
1340
1341         for node in self.entries.values():
1342             if node != self.dir:
1343                 if node != self and isinstance(node, Dir):
1344                     node.__clearRepositoryCache(duplicate)
1345                 else:
1346                     node.clear()
1347                     try:
1348                         del node._srcreps
1349                     except AttributeError:
1350                         pass
1351                     if duplicate != None:
1352                         node.duplicate=duplicate
1353
1354     def __resetDuplicate(self, node):
1355         if node != self:
1356             node.duplicate = node.get_dir().duplicate
1357
1358     def Entry(self, name):
1359         """
1360         Looks up or creates an entry node named 'name' relative to
1361         this directory.
1362         """
1363         return self.fs.Entry(name, self)
1364
1365     def Dir(self, name, create=True):
1366         """
1367         Looks up or creates a directory node named 'name' relative to
1368         this directory.
1369         """
1370         dir = self.fs.Dir(name, self, create)
1371         return dir
1372
1373     def File(self, name):
1374         """
1375         Looks up or creates a file node named 'name' relative to
1376         this directory.
1377         """
1378         return self.fs.File(name, self)
1379
1380     def _lookup_rel(self, name, klass, create=1):
1381         """
1382         Looks up a *normalized* relative path name, relative to this
1383         directory.
1384
1385         This method is intended for use by internal lookups with
1386         already-normalized path data.  For general-purpose lookups,
1387         use the Entry(), Dir() and File() methods above.
1388
1389         This method does *no* input checking and will die or give
1390         incorrect results if it's passed a non-normalized path name (e.g.,
1391         a path containing '..'), an absolute path name, a top-relative
1392         ('#foo') path name, or any kind of object.
1393         """
1394         name = self.entry_labspath(name)
1395         return self.root._lookup_abs(name, klass, create)
1396
1397     def link(self, srcdir, duplicate):
1398         """Set this directory as the variant directory for the
1399         supplied source directory."""
1400         self.srcdir = srcdir
1401         self.duplicate = duplicate
1402         self.__clearRepositoryCache(duplicate)
1403         srcdir.variant_dirs.append(self)
1404
1405     def getRepositories(self):
1406         """Returns a list of repositories for this directory.
1407         """
1408         if self.srcdir and not self.duplicate:
1409             return self.srcdir.get_all_rdirs() + self.repositories
1410         return self.repositories
1411
1412     memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
1413
1414     def get_all_rdirs(self):
1415         try:
1416             return list(self._memo['get_all_rdirs'])
1417         except KeyError:
1418             pass
1419
1420         result = [self]
1421         fname = '.'
1422         dir = self
1423         while dir:
1424             for rep in dir.getRepositories():
1425                 result.append(rep.Dir(fname))
1426             if fname == '.':
1427                 fname = dir.name
1428             else:
1429                 fname = dir.name + os.sep + fname
1430             dir = dir.up()
1431
1432         self._memo['get_all_rdirs'] = list(result)
1433
1434         return result
1435
1436     def addRepository(self, dir):
1437         if dir != self and not dir in self.repositories:
1438             self.repositories.append(dir)
1439             dir.tpath = '.'
1440             self.__clearRepositoryCache()
1441
1442     def up(self):
1443         return self.entries['..']
1444
1445     def _rel_path_key(self, other):
1446         return str(other)
1447
1448     memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
1449
1450     def rel_path(self, other):
1451         """Return a path to "other" relative to this directory.
1452         """
1453
1454         # This complicated and expensive method, which constructs relative
1455         # paths between arbitrary Node.FS objects, is no longer used
1456         # by SCons itself.  It was introduced to store dependency paths
1457         # in .sconsign files relative to the target, but that ended up
1458         # being significantly inefficient.
1459         #
1460         # We're continuing to support the method because some SConstruct
1461         # files out there started using it when it was available, and
1462         # we're all about backwards compatibility..
1463
1464         try:
1465             memo_dict = self._memo['rel_path']
1466         except KeyError:
1467             memo_dict = {}
1468             self._memo['rel_path'] = memo_dict
1469         else:
1470             try:
1471                 return memo_dict[other]
1472             except KeyError:
1473                 pass
1474
1475         if self is other:
1476
1477             result = '.'
1478
1479         elif not other in self.path_elements:
1480
1481             try:
1482                 other_dir = other.get_dir()
1483             except AttributeError:
1484                 result = str(other)
1485             else:
1486                 if other_dir is None:
1487                     result = other.name
1488                 else:
1489                     dir_rel_path = self.rel_path(other_dir)
1490                     if dir_rel_path == '.':
1491                         result = other.name
1492                     else:
1493                         result = dir_rel_path + os.sep + other.name
1494
1495         else:
1496
1497             i = self.path_elements.index(other) + 1
1498
1499             path_elems = ['..'] * (len(self.path_elements) - i) \
1500                          + map(lambda n: n.name, other.path_elements[i:])
1501              
1502             result = string.join(path_elems, os.sep)
1503
1504         memo_dict[other] = result
1505
1506         return result
1507
1508     def get_env_scanner(self, env, kw={}):
1509         import SCons.Defaults
1510         return SCons.Defaults.DirEntryScanner
1511
1512     def get_target_scanner(self):
1513         import SCons.Defaults
1514         return SCons.Defaults.DirEntryScanner
1515
1516     def get_found_includes(self, env, scanner, path):
1517         """Return this directory's implicit dependencies.
1518
1519         We don't bother caching the results because the scan typically
1520         shouldn't be requested more than once (as opposed to scanning
1521         .h file contents, which can be requested as many times as the
1522         files is #included by other files).
1523         """
1524         if not scanner:
1525             return []
1526         # Clear cached info for this Dir.  If we already visited this
1527         # directory on our walk down the tree (because we didn't know at
1528         # that point it was being used as the source for another Node)
1529         # then we may have calculated build signature before realizing
1530         # we had to scan the disk.  Now that we have to, though, we need
1531         # to invalidate the old calculated signature so that any node
1532         # dependent on our directory structure gets one that includes
1533         # info about everything on disk.
1534         self.clear()
1535         return scanner(self, env, path)
1536
1537     #
1538     # Taskmaster interface subsystem
1539     #
1540
1541     def prepare(self):
1542         pass
1543
1544     def build(self, **kw):
1545         """A null "builder" for directories."""
1546         global MkdirBuilder
1547         if not self.builder is MkdirBuilder:
1548             apply(SCons.Node.Node.build, [self,], kw)
1549
1550     #
1551     #
1552     #
1553
1554     def _create(self):
1555         """Create this directory, silently and without worrying about
1556         whether the builder is the default or not."""
1557         listDirs = []
1558         parent = self
1559         while parent:
1560             if parent.exists():
1561                 break
1562             listDirs.append(parent)
1563             p = parent.up()
1564             if p is None:
1565                 raise SCons.Errors.StopError, parent.path
1566             parent = p
1567         listDirs.reverse()
1568         for dirnode in listDirs:
1569             try:
1570                 # Don't call dirnode.build(), call the base Node method
1571                 # directly because we definitely *must* create this
1572                 # directory.  The dirnode.build() method will suppress
1573                 # the build if it's the default builder.
1574                 SCons.Node.Node.build(dirnode)
1575                 dirnode.get_executor().nullify()
1576                 # The build() action may or may not have actually
1577                 # created the directory, depending on whether the -n
1578                 # option was used or not.  Delete the _exists and
1579                 # _rexists attributes so they can be reevaluated.
1580                 dirnode.clear()
1581             except OSError:
1582                 pass
1583
1584     def multiple_side_effect_has_builder(self):
1585         global MkdirBuilder
1586         return not self.builder is MkdirBuilder and self.has_builder()
1587
1588     def alter_targets(self):
1589         """Return any corresponding targets in a variant directory.
1590         """
1591         return self.fs.variant_dir_target_climb(self, self, [])
1592
1593     def scanner_key(self):
1594         """A directory does not get scanned."""
1595         return None
1596
1597     def get_contents(self):
1598         """Return content signatures and names of all our children
1599         separated by new-lines. Ensure that the nodes are sorted."""
1600         contents = []
1601         name_cmp = lambda a, b: cmp(a.name, b.name)
1602         sorted_children = self.children()[:]
1603         sorted_children.sort(name_cmp)        
1604         for node in sorted_children:
1605             contents.append('%s %s\n' % (node.get_csig(), node.name))
1606         return string.join(contents, '')
1607
1608     def get_csig(self):
1609         """Compute the content signature for Directory nodes. In
1610         general, this is not needed and the content signature is not
1611         stored in the DirNodeInfo. However, if get_contents on a Dir
1612         node is called which has a child directory, the child
1613         directory should return the hash of its contents."""
1614         contents = self.get_contents()
1615         return SCons.Util.MD5signature(contents)
1616
1617     def do_duplicate(self, src):
1618         pass
1619
1620     changed_since_last_build = SCons.Node.Node.state_has_changed
1621
1622     def is_up_to_date(self):
1623         """If any child is not up-to-date, then this directory isn't,
1624         either."""
1625         if not self.builder is MkdirBuilder and not self.exists():
1626             return 0
1627         up_to_date = SCons.Node.up_to_date
1628         for kid in self.children():
1629             if kid.get_state() > up_to_date:
1630                 return 0
1631         return 1
1632
1633     def rdir(self):
1634         if not self.exists():
1635             norm_name = _my_normcase(self.name)
1636             for dir in self.dir.get_all_rdirs():
1637                 try: node = dir.entries[norm_name]
1638                 except KeyError: node = dir.dir_on_disk(self.name)
1639                 if node and node.exists() and \
1640                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1641                         return node
1642         return self
1643
1644     def sconsign(self):
1645         """Return the .sconsign file info for this directory,
1646         creating it first if necessary."""
1647         if not self._sconsign:
1648             import SCons.SConsign
1649             self._sconsign = SCons.SConsign.ForDirectory(self)
1650         return self._sconsign
1651
1652     def srcnode(self):
1653         """Dir has a special need for srcnode()...if we
1654         have a srcdir attribute set, then that *is* our srcnode."""
1655         if self.srcdir:
1656             return self.srcdir
1657         return Base.srcnode(self)
1658
1659     def get_timestamp(self):
1660         """Return the latest timestamp from among our children"""
1661         stamp = 0
1662         for kid in self.children():
1663             if kid.get_timestamp() > stamp:
1664                 stamp = kid.get_timestamp()
1665         return stamp
1666
1667     def entry_abspath(self, name):
1668         return self.abspath + os.sep + name
1669
1670     def entry_labspath(self, name):
1671         return self.labspath + '/' + name
1672
1673     def entry_path(self, name):
1674         return self.path + os.sep + name
1675
1676     def entry_tpath(self, name):
1677         return self.tpath + os.sep + name
1678
1679     def entry_exists_on_disk(self, name):
1680         try:
1681             d = self.on_disk_entries
1682         except AttributeError:
1683             d = {}
1684             try:
1685                 entries = os.listdir(self.abspath)
1686             except OSError:
1687                 pass
1688             else:
1689                 for entry in map(_my_normcase, entries):
1690                     d[entry] = 1
1691             self.on_disk_entries = d
1692         return d.has_key(_my_normcase(name))
1693
1694     memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
1695
1696     def srcdir_list(self):
1697         try:
1698             return self._memo['srcdir_list']
1699         except KeyError:
1700             pass
1701
1702         result = []
1703
1704         dirname = '.'
1705         dir = self
1706         while dir:
1707             if dir.srcdir:
1708                 result.append(dir.srcdir.Dir(dirname))
1709             dirname = dir.name + os.sep + dirname
1710             dir = dir.up()
1711
1712         self._memo['srcdir_list'] = result
1713
1714         return result
1715
1716     def srcdir_duplicate(self, name):
1717         for dir in self.srcdir_list():
1718             if self.is_under(dir):
1719                 # We shouldn't source from something in the build path;
1720                 # variant_dir is probably under src_dir, in which case
1721                 # we are reflecting.
1722                 break
1723             if dir.entry_exists_on_disk(name):
1724                 srcnode = dir.Entry(name).disambiguate()
1725                 if self.duplicate:
1726                     node = self.Entry(name).disambiguate()
1727                     node.do_duplicate(srcnode)
1728                     return node
1729                 else:
1730                     return srcnode
1731         return None
1732
1733     def _srcdir_find_file_key(self, filename):
1734         return filename
1735
1736     memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
1737
1738     def srcdir_find_file(self, filename):
1739         try:
1740             memo_dict = self._memo['srcdir_find_file']
1741         except KeyError:
1742             memo_dict = {}
1743             self._memo['srcdir_find_file'] = memo_dict
1744         else:
1745             try:
1746                 return memo_dict[filename]
1747             except KeyError:
1748                 pass
1749
1750         def func(node):
1751             if (isinstance(node, File) or isinstance(node, Entry)) and \
1752                (node.is_derived() or node.exists()):
1753                     return node
1754             return None
1755
1756         norm_name = _my_normcase(filename)
1757
1758         for rdir in self.get_all_rdirs():
1759             try: node = rdir.entries[norm_name]
1760             except KeyError: node = rdir.file_on_disk(filename)
1761             else: node = func(node)
1762             if node:
1763                 result = (node, self)
1764                 memo_dict[filename] = result
1765                 return result
1766
1767         for srcdir in self.srcdir_list():
1768             for rdir in srcdir.get_all_rdirs():
1769                 try: node = rdir.entries[norm_name]
1770                 except KeyError: node = rdir.file_on_disk(filename)
1771                 else: node = func(node)
1772                 if node:
1773                     result = (File(filename, self, self.fs), srcdir)
1774                     memo_dict[filename] = result
1775                     return result
1776
1777         result = (None, None)
1778         memo_dict[filename] = result
1779         return result
1780
1781     def dir_on_disk(self, name):
1782         if self.entry_exists_on_disk(name):
1783             try: return self.Dir(name)
1784             except TypeError: pass
1785         node = self.srcdir_duplicate(name)
1786         if isinstance(node, File):
1787             return None
1788         return node
1789
1790     def file_on_disk(self, name):
1791         if self.entry_exists_on_disk(name) or \
1792            diskcheck_rcs(self, name) or \
1793            diskcheck_sccs(self, name):
1794             try: return self.File(name)
1795             except TypeError: pass
1796         node = self.srcdir_duplicate(name)
1797         if isinstance(node, Dir):
1798             node = None
1799         return node
1800
1801     def walk(self, func, arg):
1802         """
1803         Walk this directory tree by calling the specified function
1804         for each directory in the tree.
1805
1806         This behaves like the os.path.walk() function, but for in-memory
1807         Node.FS.Dir objects.  The function takes the same arguments as
1808         the functions passed to os.path.walk():
1809
1810                 func(arg, dirname, fnames)
1811
1812         Except that "dirname" will actually be the directory *Node*,
1813         not the string.  The '.' and '..' entries are excluded from
1814         fnames.  The fnames list may be modified in-place to filter the
1815         subdirectories visited or otherwise impose a specific order.
1816         The "arg" argument is always passed to func() and may be used
1817         in any way (or ignored, passing None is common).
1818         """
1819         entries = self.entries
1820         names = entries.keys()
1821         names.remove('.')
1822         names.remove('..')
1823         func(arg, self, names)
1824         select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
1825         for dirname in filter(select_dirs, names):
1826             entries[dirname].walk(func, arg)
1827
1828     def glob(self, pathname, ondisk=True, source=False, strings=False):
1829         """
1830         Returns a list of Nodes (or strings) matching a specified
1831         pathname pattern.
1832
1833         Pathname patterns follow UNIX shell semantics:  * matches
1834         any-length strings of any characters, ? matches any character,
1835         and [] can enclose lists or ranges of characters.  Matches do
1836         not span directory separators.
1837
1838         The matches take into account Repositories, returning local
1839         Nodes if a corresponding entry exists in a Repository (either
1840         an in-memory Node or something on disk).
1841
1842         By defafult, the glob() function matches entries that exist
1843         on-disk, in addition to in-memory Nodes.  Setting the "ondisk"
1844         argument to False (or some other non-true value) causes the glob()
1845         function to only match in-memory Nodes.  The default behavior is
1846         to return both the on-disk and in-memory Nodes.
1847
1848         The "source" argument, when true, specifies that corresponding
1849         source Nodes must be returned if you're globbing in a build
1850         directory (initialized with VariantDir()).  The default behavior
1851         is to return Nodes local to the VariantDir().
1852
1853         The "strings" argument, when true, returns the matches as strings,
1854         not Nodes.  The strings are path names relative to this directory.
1855
1856         The underlying algorithm is adapted from the glob.glob() function
1857         in the Python library (but heavily modified), and uses fnmatch()
1858         under the covers.
1859         """
1860         dirname, basename = os.path.split(pathname)
1861         if not dirname:
1862             return self._glob1(basename, ondisk, source, strings)
1863         if has_glob_magic(dirname):
1864             list = self.glob(dirname, ondisk, source, strings=False)
1865         else:
1866             list = [self.Dir(dirname, create=True)]
1867         result = []
1868         for dir in list:
1869             r = dir._glob1(basename, ondisk, source, strings)
1870             if strings:
1871                 r = map(lambda x, d=str(dir): os.path.join(d, x), r)
1872             result.extend(r)
1873         result.sort(lambda a, b: cmp(str(a), str(b)))
1874         return result
1875
1876     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
1877         """
1878         Globs for and returns a list of entry names matching a single
1879         pattern in this directory.
1880
1881         This searches any repositories and source directories for
1882         corresponding entries and returns a Node (or string) relative
1883         to the current directory if an entry is found anywhere.
1884
1885         TODO: handle pattern with no wildcard
1886         """
1887         search_dir_list = self.get_all_rdirs()
1888         for srcdir in self.srcdir_list():
1889             search_dir_list.extend(srcdir.get_all_rdirs())
1890
1891         names = []
1892         for dir in search_dir_list:
1893             # We use the .name attribute from the Node because the keys of
1894             # the dir.entries dictionary are normalized (that is, all upper
1895             # case) on case-insensitive systems like Windows.
1896             #node_names = [ v.name for k, v in dir.entries.items() if k not in ('.', '..') ]
1897             entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys())
1898             node_names = map(lambda n, e=dir.entries: e[n].name, entry_names)
1899             names.extend(node_names)
1900             if ondisk:
1901                 try:
1902                     disk_names = os.listdir(dir.abspath)
1903                 except os.error:
1904                     pass
1905                 else:
1906                     names.extend(disk_names)
1907                     if not strings:
1908                         # We're going to return corresponding Nodes in
1909                         # the local directory, so we need to make sure
1910                         # those Nodes exist.  We only want to create
1911                         # Nodes for the entries that will match the
1912                         # specified pattern, though, which means we
1913                         # need to filter the list here, even though
1914                         # the overall list will also be filtered later,
1915                         # after we exit this loop.
1916                         if pattern[0] != '.':
1917                             #disk_names = [ d for d in disk_names if d[0] != '.' ]
1918                             disk_names = filter(lambda x: x[0] != '.', disk_names)
1919                         disk_names = fnmatch.filter(disk_names, pattern)
1920                         rep_nodes = map(dir.Entry, disk_names)
1921                         #rep_nodes = [ n.disambiguate() for n in rep_nodes ]
1922                         rep_nodes = map(lambda n: n.disambiguate(), rep_nodes)
1923                         for node, name in izip(rep_nodes, disk_names):
1924                             n = self.Entry(name)
1925                             if n.__class__ != node.__class__:
1926                                 n.__class__ = node.__class__
1927                                 n._morph()
1928
1929         names = set(names)
1930         if pattern[0] != '.':
1931             #names = [ n for n in names if n[0] != '.' ]
1932             names = filter(lambda x: x[0] != '.', names)
1933         names = fnmatch.filter(names, pattern)
1934
1935         if strings:
1936             return names
1937
1938         #return [ self.entries[_my_normcase(n)] for n in names ]
1939         return map(lambda n, e=self.entries:  e[_my_normcase(n)], names)
1940
1941 class RootDir(Dir):
1942     """A class for the root directory of a file system.
1943
1944     This is the same as a Dir class, except that the path separator
1945     ('/' or '\\') is actually part of the name, so we don't need to
1946     add a separator when creating the path names of entries within
1947     this directory.
1948     """
1949     def __init__(self, name, fs):
1950         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1951         # We're going to be our own parent directory (".." entry and .dir
1952         # attribute) so we have to set up some values so Base.__init__()
1953         # won't gag won't it calls some of our methods.
1954         self.abspath = ''
1955         self.labspath = ''
1956         self.path = ''
1957         self.tpath = ''
1958         self.path_elements = []
1959         self.duplicate = 0
1960         self.root = self
1961         Base.__init__(self, name, self, fs)
1962
1963         # Now set our paths to what we really want them to be: the
1964         # initial drive letter (the name) plus the directory separator,
1965         # except for the "lookup abspath," which does not have the
1966         # drive letter.
1967         self.abspath = name + os.sep
1968         self.labspath = ''
1969         self.path = name + os.sep
1970         self.tpath = name + os.sep
1971         self._morph()
1972
1973         self._lookupDict = {}
1974
1975         # The // and os.sep + os.sep entries are necessary because
1976         # os.path.normpath() seems to preserve double slashes at the
1977         # beginning of a path (presumably for UNC path names), but
1978         # collapses triple slashes to a single slash.
1979         self._lookupDict[''] = self
1980         self._lookupDict['/'] = self
1981         self._lookupDict['//'] = self
1982         self._lookupDict[os.sep] = self
1983         self._lookupDict[os.sep + os.sep] = self
1984
1985     def must_be_same(self, klass):
1986         if klass is Dir:
1987             return
1988         Base.must_be_same(self, klass)
1989
1990     def _lookup_abs(self, p, klass, create=1):
1991         """
1992         Fast (?) lookup of a *normalized* absolute path.
1993
1994         This method is intended for use by internal lookups with
1995         already-normalized path data.  For general-purpose lookups,
1996         use the FS.Entry(), FS.Dir() or FS.File() methods.
1997
1998         The caller is responsible for making sure we're passed a
1999         normalized absolute path; we merely let Python's dictionary look
2000         up and return the One True Node.FS object for the path.
2001
2002         If no Node for the specified "p" doesn't already exist, and
2003         "create" is specified, the Node may be created after recursive
2004         invocation to find or create the parent directory or directories.
2005         """
2006         k = _my_normcase(p)
2007         try:
2008             result = self._lookupDict[k]
2009         except KeyError:
2010             if not create:
2011                 raise SCons.Errors.UserError
2012             # There is no Node for this path name, and we're allowed
2013             # to create it.
2014             dir_name, file_name = os.path.split(p)
2015             dir_node = self._lookup_abs(dir_name, Dir)
2016             result = klass(file_name, dir_node, self.fs)
2017
2018             # Double-check on disk (as configured) that the Node we
2019             # created matches whatever is out there in the real world.
2020             result.diskcheck_match()
2021
2022             self._lookupDict[k] = result
2023             dir_node.entries[_my_normcase(file_name)] = result
2024             dir_node.implicit = None
2025         else:
2026             # There is already a Node for this path name.  Allow it to
2027             # complain if we were looking for an inappropriate type.
2028             result.must_be_same(klass)
2029         return result
2030
2031     def __str__(self):
2032         return self.abspath
2033
2034     def entry_abspath(self, name):
2035         return self.abspath + name
2036
2037     def entry_labspath(self, name):
2038         return '/' + name
2039
2040     def entry_path(self, name):
2041         return self.path + name
2042
2043     def entry_tpath(self, name):
2044         return self.tpath + name
2045
2046     def is_under(self, dir):
2047         if self is dir:
2048             return 1
2049         else:
2050             return 0
2051
2052     def up(self):
2053         return None
2054
2055     def get_dir(self):
2056         return None
2057
2058     def src_builder(self):
2059         return _null
2060
2061 class FileNodeInfo(SCons.Node.NodeInfoBase):
2062     current_version_id = 1
2063
2064     field_list = ['csig', 'timestamp', 'size']
2065
2066     # This should get reset by the FS initialization.
2067     fs = None
2068
2069     def str_to_node(self, s):
2070         top = self.fs.Top
2071         root = top.root
2072         if do_splitdrive:
2073             drive, s = os.path.splitdrive(s)
2074             if drive:
2075                 root = self.fs.get_root(drive)
2076         if not os.path.isabs(s):
2077             s = top.labspath + '/' + s
2078         return root._lookup_abs(s, Entry)
2079
2080 class FileBuildInfo(SCons.Node.BuildInfoBase):
2081     current_version_id = 1
2082
2083     def convert_to_sconsign(self):
2084         """
2085         Converts this FileBuildInfo object for writing to a .sconsign file
2086
2087         This replaces each Node in our various dependency lists with its
2088         usual string representation: relative to the top-level SConstruct
2089         directory, or an absolute path if it's outside.
2090         """
2091         if os.sep == '/':
2092             node_to_str = str
2093         else:
2094             def node_to_str(n):
2095                 try:
2096                     s = n.path
2097                 except AttributeError:
2098                     s = str(n)
2099                 else:
2100                     s = string.replace(s, os.sep, '/')
2101                 return s
2102         for attr in ['bsources', 'bdepends', 'bimplicit']:
2103             try:
2104                 val = getattr(self, attr)
2105             except AttributeError:
2106                 pass
2107             else:
2108                 setattr(self, attr, map(node_to_str, val))
2109     def convert_from_sconsign(self, dir, name):
2110         """
2111         Converts a newly-read FileBuildInfo object for in-SCons use
2112
2113         For normal up-to-date checking, we don't have any conversion to
2114         perform--but we're leaving this method here to make that clear.
2115         """
2116         pass
2117     def prepare_dependencies(self):
2118         """
2119         Prepares a FileBuildInfo object for explaining what changed
2120
2121         The bsources, bdepends and bimplicit lists have all been
2122         stored on disk as paths relative to the top-level SConstruct
2123         directory.  Convert the strings to actual Nodes (for use by the
2124         --debug=explain code and --implicit-cache).
2125         """
2126         attrs = [
2127             ('bsources', 'bsourcesigs'),
2128             ('bdepends', 'bdependsigs'),
2129             ('bimplicit', 'bimplicitsigs'),
2130         ]
2131         for (nattr, sattr) in attrs:
2132             try:
2133                 strings = getattr(self, nattr)
2134                 nodeinfos = getattr(self, sattr)
2135             except AttributeError:
2136                 pass
2137             else:
2138                 nodes = []
2139                 for s, ni in izip(strings, nodeinfos):
2140                     if not isinstance(s, SCons.Node.Node):
2141                         s = ni.str_to_node(s)
2142                     nodes.append(s)
2143                 setattr(self, nattr, nodes)
2144     def format(self, names=0):
2145         result = []
2146         bkids = self.bsources + self.bdepends + self.bimplicit
2147         bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
2148         for bkid, bkidsig in izip(bkids, bkidsigs):
2149             result.append(str(bkid) + ': ' +
2150                           string.join(bkidsig.format(names=names), ' '))
2151         result.append('%s [%s]' % (self.bactsig, self.bact))
2152         return string.join(result, '\n')
2153
2154 class File(Base):
2155     """A class for files in a file system.
2156     """
2157
2158     memoizer_counters = []
2159
2160     NodeInfo = FileNodeInfo
2161     BuildInfo = FileBuildInfo
2162
2163     def diskcheck_match(self):
2164         diskcheck_match(self, self.isdir,
2165                         "Directory %s found where file expected.")
2166
2167     def __init__(self, name, directory, fs):
2168         if __debug__: logInstanceCreation(self, 'Node.FS.File')
2169         Base.__init__(self, name, directory, fs)
2170         self._morph()
2171
2172     def Entry(self, name):
2173         """Create an entry node named 'name' relative to
2174         the SConscript directory of this file."""
2175         cwd = self.cwd or self.fs._cwd
2176         return cwd.Entry(name)
2177
2178     def Dir(self, name, create=True):
2179         """Create a directory node named 'name' relative to
2180         the SConscript directory of this file."""
2181         cwd = self.cwd or self.fs._cwd
2182         return cwd.Dir(name, create)
2183
2184     def Dirs(self, pathlist):
2185         """Create a list of directories relative to the SConscript
2186         directory of this file."""
2187         return map(lambda p, s=self: s.Dir(p), pathlist)
2188
2189     def File(self, name):
2190         """Create a file node named 'name' relative to
2191         the SConscript directory of this file."""
2192         cwd = self.cwd or self.fs._cwd
2193         return cwd.File(name)
2194
2195     #def generate_build_dict(self):
2196     #    """Return an appropriate dictionary of values for building
2197     #    this File."""
2198     #    return {'Dir' : self.Dir,
2199     #            'File' : self.File,
2200     #            'RDirs' : self.RDirs}
2201
2202     def _morph(self):
2203         """Turn a file system node into a File object."""
2204         self.scanner_paths = {}
2205         if not hasattr(self, '_local'):
2206             self._local = 0
2207
2208         # If there was already a Builder set on this entry, then
2209         # we need to make sure we call the target-decider function,
2210         # not the source-decider.  Reaching in and doing this by hand
2211         # is a little bogus.  We'd prefer to handle this by adding
2212         # an Entry.builder_set() method that disambiguates like the
2213         # other methods, but that starts running into problems with the
2214         # fragile way we initialize Dir Nodes with their Mkdir builders,
2215         # yet still allow them to be overridden by the user.  Since it's
2216         # not clear right now how to fix that, stick with what works
2217         # until it becomes clear...
2218         if self.has_builder():
2219             self.changed_since_last_build = self.decide_target
2220
2221     def scanner_key(self):
2222         return self.get_suffix()
2223
2224     def get_contents(self):
2225         if not self.rexists():
2226             return ''
2227         fname = self.rfile().abspath
2228         try:
2229             r = open(fname, "rb").read()
2230         except EnvironmentError, e:
2231             if not e.filename:
2232                 e.filename = fname
2233             raise
2234         return r
2235
2236     memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
2237
2238     def get_size(self):
2239         try:
2240             return self._memo['get_size']
2241         except KeyError:
2242             pass
2243
2244         if self.rexists():
2245             size = self.rfile().getsize()
2246         else:
2247             size = 0
2248
2249         self._memo['get_size'] = size
2250
2251         return size
2252
2253     memoizer_counters.append(SCons.Memoize.CountValue('get_timestamp'))
2254
2255     def get_timestamp(self):
2256         try:
2257             return self._memo['get_timestamp']
2258         except KeyError:
2259             pass
2260
2261         if self.rexists():
2262             timestamp = self.rfile().getmtime()
2263         else:
2264             timestamp = 0
2265
2266         self._memo['get_timestamp'] = timestamp
2267
2268         return timestamp
2269
2270     def store_info(self):
2271         # Merge our build information into the already-stored entry.
2272         # This accomodates "chained builds" where a file that's a target
2273         # in one build (SConstruct file) is a source in a different build.
2274         # See test/chained-build.py for the use case.
2275         if do_store_info:
2276             self.dir.sconsign().store_info(self.name, self)
2277
2278     convert_copy_attrs = [
2279         'bsources',
2280         'bimplicit',
2281         'bdepends',
2282         'bact',
2283         'bactsig',
2284         'ninfo',
2285     ]
2286
2287
2288     convert_sig_attrs = [
2289         'bsourcesigs',
2290         'bimplicitsigs',
2291         'bdependsigs',
2292     ]
2293
2294     def convert_old_entry(self, old_entry):
2295         # Convert a .sconsign entry from before the Big Signature
2296         # Refactoring, doing what we can to convert its information
2297         # to the new .sconsign entry format.
2298         #
2299         # The old format looked essentially like this:
2300         #
2301         #   BuildInfo
2302         #       .ninfo (NodeInfo)
2303         #           .bsig
2304         #           .csig
2305         #           .timestamp
2306         #           .size
2307         #       .bsources
2308         #       .bsourcesigs ("signature" list)
2309         #       .bdepends
2310         #       .bdependsigs ("signature" list)
2311         #       .bimplicit
2312         #       .bimplicitsigs ("signature" list)
2313         #       .bact
2314         #       .bactsig
2315         #
2316         # The new format looks like this:
2317         #
2318         #   .ninfo (NodeInfo)
2319         #       .bsig
2320         #       .csig
2321         #       .timestamp
2322         #       .size
2323         #   .binfo (BuildInfo)
2324         #       .bsources
2325         #       .bsourcesigs (NodeInfo list)
2326         #           .bsig
2327         #           .csig
2328         #           .timestamp
2329         #           .size
2330         #       .bdepends
2331         #       .bdependsigs (NodeInfo list)
2332         #           .bsig
2333         #           .csig
2334         #           .timestamp
2335         #           .size
2336         #       .bimplicit
2337         #       .bimplicitsigs (NodeInfo list)
2338         #           .bsig
2339         #           .csig
2340         #           .timestamp
2341         #           .size
2342         #       .bact
2343         #       .bactsig
2344         #
2345         # The basic idea of the new structure is that a NodeInfo always
2346         # holds all available information about the state of a given Node
2347         # at a certain point in time.  The various .b*sigs lists can just
2348         # be a list of pointers to the .ninfo attributes of the different
2349         # dependent nodes, without any copying of information until it's
2350         # time to pickle it for writing out to a .sconsign file.
2351         #
2352         # The complicating issue is that the *old* format only stored one
2353         # "signature" per dependency, based on however the *last* build
2354         # was configured.  We don't know from just looking at it whether
2355         # it was a build signature, a content signature, or a timestamp
2356         # "signature".  Since we no longer use build signatures, the
2357         # best we can do is look at the length and if it's thirty two,
2358         # assume that it was (or might have been) a content signature.
2359         # If it was actually a build signature, then it will cause a
2360         # rebuild anyway when it doesn't match the new content signature,
2361         # but that's probably the best we can do.
2362         import SCons.SConsign
2363         new_entry = SCons.SConsign.SConsignEntry()
2364         new_entry.binfo = self.new_binfo()
2365         binfo = new_entry.binfo
2366         for attr in self.convert_copy_attrs:
2367             try:
2368                 value = getattr(old_entry, attr)
2369             except AttributeError:
2370                 pass
2371             else:
2372                 setattr(binfo, attr, value)
2373                 delattr(old_entry, attr)
2374         for attr in self.convert_sig_attrs:
2375             try:
2376                 sig_list = getattr(old_entry, attr)
2377             except AttributeError:
2378                 pass
2379             else:
2380                 value = []
2381                 for sig in sig_list:
2382                     ninfo = self.new_ninfo()
2383                     if len(sig) == 32:
2384                         ninfo.csig = sig
2385                     else:
2386                         ninfo.timestamp = sig
2387                     value.append(ninfo)
2388                 setattr(binfo, attr, value)
2389                 delattr(old_entry, attr)
2390         return new_entry
2391
2392     memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
2393
2394     def get_stored_info(self):
2395         try:
2396             return self._memo['get_stored_info']
2397         except KeyError:
2398             pass
2399
2400         try:
2401             sconsign_entry = self.dir.sconsign().get_entry(self.name)
2402         except (KeyError, EnvironmentError):
2403             import SCons.SConsign
2404             sconsign_entry = SCons.SConsign.SConsignEntry()
2405             sconsign_entry.binfo = self.new_binfo()
2406             sconsign_entry.ninfo = self.new_ninfo()
2407         else:
2408             if isinstance(sconsign_entry, FileBuildInfo):
2409                 # This is a .sconsign file from before the Big Signature
2410                 # Refactoring; convert it as best we can.
2411                 sconsign_entry = self.convert_old_entry(sconsign_entry)
2412             try:
2413                 delattr(sconsign_entry.ninfo, 'bsig')
2414             except AttributeError:
2415                 pass
2416
2417         self._memo['get_stored_info'] = sconsign_entry
2418
2419         return sconsign_entry
2420
2421     def get_stored_implicit(self):
2422         binfo = self.get_stored_info().binfo
2423         binfo.prepare_dependencies()
2424         try: return binfo.bimplicit
2425         except AttributeError: return None
2426
2427     def rel_path(self, other):
2428         return self.dir.rel_path(other)
2429
2430     def _get_found_includes_key(self, env, scanner, path):
2431         return (id(env), id(scanner), path)
2432
2433     memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
2434
2435     def get_found_includes(self, env, scanner, path):
2436         """Return the included implicit dependencies in this file.
2437         Cache results so we only scan the file once per path
2438         regardless of how many times this information is requested.
2439         """
2440         memo_key = (id(env), id(scanner), path)
2441         try:
2442             memo_dict = self._memo['get_found_includes']
2443         except KeyError:
2444             memo_dict = {}
2445             self._memo['get_found_includes'] = memo_dict
2446         else:
2447             try:
2448                 return memo_dict[memo_key]
2449             except KeyError:
2450                 pass
2451
2452         if scanner:
2453             result = scanner(self, env, path)
2454             result = map(lambda N: N.disambiguate(), result)
2455         else:
2456             result = []
2457
2458         memo_dict[memo_key] = result
2459
2460         return result
2461
2462     def _createDir(self):
2463         # ensure that the directories for this node are
2464         # created.
2465         self.dir._create()
2466
2467     def retrieve_from_cache(self):
2468         """Try to retrieve the node's content from a cache
2469
2470         This method is called from multiple threads in a parallel build,
2471         so only do thread safe stuff here. Do thread unsafe stuff in
2472         built().
2473
2474         Returns true iff the node was successfully retrieved.
2475         """
2476         if self.nocache:
2477             return None
2478         if not self.is_derived():
2479             return None
2480         return self.get_build_env().get_CacheDir().retrieve(self)
2481
2482     def built(self):
2483         """
2484         Called just after this node is successfully built.
2485         """
2486         # Push this file out to cache before the superclass Node.built()
2487         # method has a chance to clear the build signature, which it
2488         # will do if this file has a source scanner.
2489         #
2490         # We have to clear the memoized values *before* we push it to
2491         # cache so that the memoization of the self.exists() return
2492         # value doesn't interfere.
2493         self.clear_memoized_values()
2494         if self.exists():
2495             self.get_build_env().get_CacheDir().push(self)
2496         SCons.Node.Node.built(self)
2497
2498     def visited(self):
2499         if self.exists():
2500             self.get_build_env().get_CacheDir().push_if_forced(self)
2501
2502         ninfo = self.get_ninfo()
2503
2504         csig = self.get_max_drift_csig()
2505         if csig:
2506             ninfo.csig = csig
2507
2508         ninfo.timestamp = self.get_timestamp()
2509         ninfo.size      = self.get_size()
2510
2511         if not self.has_builder():
2512             # This is a source file, but it might have been a target file
2513             # in another build that included more of the DAG.  Copy
2514             # any build information that's stored in the .sconsign file
2515             # into our binfo object so it doesn't get lost.
2516             old = self.get_stored_info()
2517             self.get_binfo().__dict__.update(old.binfo.__dict__)
2518
2519         self.store_info()
2520
2521     def find_src_builder(self):
2522         if self.rexists():
2523             return None
2524         scb = self.dir.src_builder()
2525         if scb is _null:
2526             if diskcheck_sccs(self.dir, self.name):
2527                 scb = get_DefaultSCCSBuilder()
2528             elif diskcheck_rcs(self.dir, self.name):
2529                 scb = get_DefaultRCSBuilder()
2530             else:
2531                 scb = None
2532         if scb is not None:
2533             try:
2534                 b = self.builder
2535             except AttributeError:
2536                 b = None
2537             if b is None:
2538                 self.builder_set(scb)
2539         return scb
2540
2541     def has_src_builder(self):
2542         """Return whether this Node has a source builder or not.
2543
2544         If this Node doesn't have an explicit source code builder, this
2545         is where we figure out, on the fly, if there's a transparent
2546         source code builder for it.
2547
2548         Note that if we found a source builder, we also set the
2549         self.builder attribute, so that all of the methods that actually
2550         *build* this file don't have to do anything different.
2551         """
2552         try:
2553             scb = self.sbuilder
2554         except AttributeError:
2555             scb = self.sbuilder = self.find_src_builder()
2556         return not scb is None
2557
2558     def alter_targets(self):
2559         """Return any corresponding targets in a variant directory.
2560         """
2561         if self.is_derived():
2562             return [], None
2563         return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
2564
2565     def _rmv_existing(self):
2566         self.clear_memoized_values()
2567         e = Unlink(self, [], None)
2568         if isinstance(e, SCons.Errors.BuildError):
2569             raise e
2570
2571     #
2572     # Taskmaster interface subsystem
2573     #
2574
2575     def make_ready(self):
2576         self.has_src_builder()
2577         self.get_binfo()
2578
2579     def prepare(self):
2580         """Prepare for this file to be created."""
2581         SCons.Node.Node.prepare(self)
2582
2583         if self.get_state() != SCons.Node.up_to_date:
2584             if self.exists():
2585                 if self.is_derived() and not self.precious:
2586                     self._rmv_existing()
2587             else:
2588                 try:
2589                     self._createDir()
2590                 except SCons.Errors.StopError, drive:
2591                     desc = "No drive `%s' for target `%s'." % (drive, self)
2592                     raise SCons.Errors.StopError, desc
2593
2594     #
2595     #
2596     #
2597
2598     def remove(self):
2599         """Remove this file."""
2600         if self.exists() or self.islink():
2601             self.fs.unlink(self.path)
2602             return 1
2603         return None
2604
2605     def do_duplicate(self, src):
2606         self._createDir()
2607         Unlink(self, None, None)
2608         e = Link(self, src, None)
2609         if isinstance(e, SCons.Errors.BuildError):
2610             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
2611             raise SCons.Errors.StopError, desc
2612         self.linked = 1
2613         # The Link() action may or may not have actually
2614         # created the file, depending on whether the -n
2615         # option was used or not.  Delete the _exists and
2616         # _rexists attributes so they can be reevaluated.
2617         self.clear()
2618
2619     memoizer_counters.append(SCons.Memoize.CountValue('exists'))
2620
2621     def exists(self):
2622         try:
2623             return self._memo['exists']
2624         except KeyError:
2625             pass
2626         # Duplicate from source path if we are set up to do this.
2627         if self.duplicate and not self.is_derived() and not self.linked:
2628             src = self.srcnode()
2629             if not src is self:
2630                 # At this point, src is meant to be copied in a variant directory.
2631                 src = src.rfile()
2632                 if src.abspath != self.abspath:
2633                     if src.exists():
2634                         self.do_duplicate(src)
2635                         # Can't return 1 here because the duplication might
2636                         # not actually occur if the -n option is being used.
2637                     else:
2638                         # The source file does not exist.  Make sure no old
2639                         # copy remains in the variant directory.
2640                         if Base.exists(self) or self.islink():
2641                             self.fs.unlink(self.path)
2642                         # Return None explicitly because the Base.exists() call
2643                         # above will have cached its value if the file existed.
2644                         self._memo['exists'] = None
2645                         return None
2646         result = Base.exists(self)
2647         self._memo['exists'] = result
2648         return result
2649
2650     #
2651     # SIGNATURE SUBSYSTEM
2652     #
2653
2654     def get_max_drift_csig(self):
2655         """
2656         Returns the content signature currently stored for this node
2657         if it's been unmodified longer than the max_drift value, or the
2658         max_drift value is 0.  Returns None otherwise.
2659         """
2660         old = self.get_stored_info()
2661         mtime = self.get_timestamp()
2662
2663         csig = None
2664         max_drift = self.fs.max_drift
2665         if max_drift > 0:
2666             if (time.time() - mtime) > max_drift:
2667                 try:
2668                     n = old.ninfo
2669                     if n.timestamp and n.csig and n.timestamp == mtime:
2670                         csig = n.csig
2671                 except AttributeError:
2672                     pass
2673         elif max_drift == 0:
2674             try:
2675                 csig = old.ninfo.csig
2676             except AttributeError:
2677                 pass
2678
2679         return csig
2680
2681     def get_csig(self):
2682         """
2683         Generate a node's content signature, the digested signature
2684         of its content.
2685
2686         node - the node
2687         cache - alternate node to use for the signature cache
2688         returns - the content signature
2689         """
2690         ninfo = self.get_ninfo()
2691         try:
2692             return ninfo.csig
2693         except AttributeError:
2694             pass
2695
2696         csig = self.get_max_drift_csig()
2697         if csig is None:
2698
2699             try:
2700                 contents = self.get_contents()
2701             except IOError:
2702                 # This can happen if there's actually a directory on-disk,
2703                 # which can be the case if they've disabled disk checks,
2704                 # or if an action with a File target actually happens to
2705                 # create a same-named directory by mistake.
2706                 csig = ''
2707             else:
2708                 csig = SCons.Util.MD5signature(contents)
2709
2710         ninfo.csig = csig
2711
2712         return csig
2713
2714     #
2715     # DECISION SUBSYSTEM
2716     #
2717
2718     def builder_set(self, builder):
2719         SCons.Node.Node.builder_set(self, builder)
2720         self.changed_since_last_build = self.decide_target
2721
2722     def changed_content(self, target, prev_ni):
2723         cur_csig = self.get_csig()
2724         try:
2725             return cur_csig != prev_ni.csig
2726         except AttributeError:
2727             return 1
2728
2729     def changed_state(self, target, prev_ni):
2730         return (self.state != SCons.Node.up_to_date)
2731
2732     def changed_timestamp_then_content(self, target, prev_ni):
2733         if not self.changed_timestamp_match(target, prev_ni):
2734             try:
2735                 self.get_ninfo().csig = prev_ni.csig
2736             except AttributeError:
2737                 pass
2738             return False
2739         return self.changed_content(target, prev_ni)
2740
2741     def changed_timestamp_newer(self, target, prev_ni):
2742         try:
2743             return self.get_timestamp() > target.get_timestamp()
2744         except AttributeError:
2745             return 1
2746
2747     def changed_timestamp_match(self, target, prev_ni):
2748         try:
2749             return self.get_timestamp() != prev_ni.timestamp
2750         except AttributeError:
2751             return 1
2752
2753     def decide_source(self, target, prev_ni):
2754         return target.get_build_env().decide_source(self, target, prev_ni)
2755
2756     def decide_target(self, target, prev_ni):
2757         return target.get_build_env().decide_target(self, target, prev_ni)
2758
2759     # Initialize this Node's decider function to decide_source() because
2760     # every file is a source file until it has a Builder attached...
2761     changed_since_last_build = decide_source
2762
2763     def is_up_to_date(self):
2764         T = 0
2765         if T: Trace('is_up_to_date(%s):' % self)
2766         if not self.exists():
2767             if T: Trace(' not self.exists():')
2768             # The file doesn't exist locally...
2769             r = self.rfile()
2770             if r != self:
2771                 # ...but there is one in a Repository...
2772                 if not self.changed(r):
2773                     if T: Trace(' changed(%s):' % r)
2774                     # ...and it's even up-to-date...
2775                     if self._local:
2776                         # ...and they'd like a local copy.
2777                         e = LocalCopy(self, r, None)
2778                         if isinstance(e, SCons.Errors.BuildError):
2779                             raise 
2780                         self.store_info()
2781                     if T: Trace(' 1\n')
2782                     return 1
2783             self.changed()
2784             if T: Trace(' None\n')
2785             return None
2786         else:
2787             r = self.changed()
2788             if T: Trace(' self.exists():  %s\n' % r)
2789             return not r
2790
2791     memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
2792
2793     def rfile(self):
2794         try:
2795             return self._memo['rfile']
2796         except KeyError:
2797             pass
2798         result = self
2799         if not self.exists():
2800             norm_name = _my_normcase(self.name)
2801             for dir in self.dir.get_all_rdirs():
2802                 try: node = dir.entries[norm_name]
2803                 except KeyError: node = dir.file_on_disk(self.name)
2804                 if node and node.exists() and \
2805                    (isinstance(node, File) or isinstance(node, Entry) \
2806                     or not node.is_derived()):
2807                         result = node
2808                         break
2809         self._memo['rfile'] = result
2810         return result
2811
2812     def rstr(self):
2813         return str(self.rfile())
2814
2815     def get_cachedir_csig(self):
2816         """
2817         Fetch a Node's content signature for purposes of computing
2818         another Node's cachesig.
2819
2820         This is a wrapper around the normal get_csig() method that handles
2821         the somewhat obscure case of using CacheDir with the -n option.
2822         Any files that don't exist would normally be "built" by fetching
2823         them from the cache, but the normal get_csig() method will try
2824         to open up the local file, which doesn't exist because the -n
2825         option meant we didn't actually pull the file from cachedir.
2826         But since the file *does* actually exist in the cachedir, we
2827         can use its contents for the csig.
2828         """
2829         try:
2830             return self.cachedir_csig
2831         except AttributeError:
2832             pass
2833
2834         cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
2835         if not self.exists() and cachefile and os.path.exists(cachefile):
2836             contents = open(cachefile, 'rb').read()
2837             self.cachedir_csig = SCons.Util.MD5signature(contents)
2838         else:
2839             self.cachedir_csig = self.get_csig()
2840         return self.cachedir_csig
2841
2842     def get_cachedir_bsig(self):
2843         try:
2844             return self.cachesig
2845         except AttributeError:
2846             pass
2847
2848         # Add the path to the cache signature, because multiple
2849         # targets built by the same action will all have the same
2850         # build signature, and we have to differentiate them somehow.
2851         children =  self.children()
2852         sigs = map(lambda n: n.get_cachedir_csig(), children)
2853         executor = self.get_executor()
2854         sigs.append(SCons.Util.MD5signature(executor.get_contents()))
2855         sigs.append(self.path)
2856         self.cachesig = SCons.Util.MD5collect(sigs)
2857         return self.cachesig
2858
2859 default_fs = None
2860
2861 def get_default_fs():
2862     global default_fs
2863     if not default_fs:
2864         default_fs = FS()
2865     return default_fs
2866
2867 class FileFinder:
2868     """
2869     """
2870     if SCons.Memoize.use_memoizer:
2871         __metaclass__ = SCons.Memoize.Memoized_Metaclass
2872
2873     memoizer_counters = []
2874
2875     def __init__(self):
2876         self._memo = {}
2877
2878     def filedir_lookup(self, p, fd=None):
2879         """
2880         A helper method for find_file() that looks up a directory for
2881         a file we're trying to find.  This only creates the Dir Node if
2882         it exists on-disk, since if the directory doesn't exist we know
2883         we won't find any files in it...  :-)
2884
2885         It would be more compact to just use this as a nested function
2886         with a default keyword argument (see the commented-out version
2887         below), but that doesn't work unless you have nested scopes,
2888         so we define it here just so this work under Python 1.5.2.
2889         """
2890         if fd is None:
2891             fd = self.default_filedir
2892         dir, name = os.path.split(fd)
2893         drive, d = os.path.splitdrive(dir)
2894         if d in ('/', os.sep):
2895             return p.fs.get_root(drive).dir_on_disk(name)
2896         if dir:
2897             p = self.filedir_lookup(p, dir)
2898             if not p:
2899                 return None
2900         norm_name = _my_normcase(name)
2901         try:
2902             node = p.entries[norm_name]
2903         except KeyError:
2904             return p.dir_on_disk(name)
2905         if isinstance(node, Dir):
2906             return node
2907         if isinstance(node, Entry):
2908             node.must_be_same(Dir)
2909             return node
2910         return None
2911
2912     def _find_file_key(self, filename, paths, verbose=None):
2913         return (filename, paths)
2914         
2915     memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
2916
2917     def find_file(self, filename, paths, verbose=None):
2918         """
2919         find_file(str, [Dir()]) -> [nodes]
2920
2921         filename - a filename to find
2922         paths - a list of directory path *nodes* to search in.  Can be
2923                 represented as a list, a tuple, or a callable that is
2924                 called with no arguments and returns the list or tuple.
2925
2926         returns - the node created from the found file.
2927
2928         Find a node corresponding to either a derived file or a file
2929         that exists already.
2930
2931         Only the first file found is returned, and none is returned
2932         if no file is found.
2933         """
2934         memo_key = self._find_file_key(filename, paths)
2935         try:
2936             memo_dict = self._memo['find_file']
2937         except KeyError:
2938             memo_dict = {}
2939             self._memo['find_file'] = memo_dict
2940         else:
2941             try:
2942                 return memo_dict[memo_key]
2943             except KeyError:
2944                 pass
2945
2946         if verbose:
2947             if not SCons.Util.is_String(verbose):
2948                 verbose = "find_file"
2949             if not callable(verbose):
2950                 verbose = '  %s: ' % verbose
2951                 verbose = lambda s, v=verbose: sys.stdout.write(v + s)
2952         else:
2953             verbose = lambda x: x
2954
2955         filedir, filename = os.path.split(filename)
2956         if filedir:
2957             # More compact code that we can't use until we drop
2958             # support for Python 1.5.2:
2959             #
2960             #def filedir_lookup(p, fd=filedir):
2961             #    """
2962             #    A helper function that looks up a directory for a file
2963             #    we're trying to find.  This only creates the Dir Node
2964             #    if it exists on-disk, since if the directory doesn't
2965             #    exist we know we won't find any files in it...  :-)
2966             #    """
2967             #    dir, name = os.path.split(fd)
2968             #    if dir:
2969             #        p = filedir_lookup(p, dir)
2970             #        if not p:
2971             #            return None
2972             #    norm_name = _my_normcase(name)
2973             #    try:
2974             #        node = p.entries[norm_name]
2975             #    except KeyError:
2976             #        return p.dir_on_disk(name)
2977             #    if isinstance(node, Dir):
2978             #        return node
2979             #    if isinstance(node, Entry):
2980             #        node.must_be_same(Dir)
2981             #        return node
2982             #    if isinstance(node, Dir) or isinstance(node, Entry):
2983             #        return node
2984             #    return None
2985             #paths = filter(None, map(filedir_lookup, paths))
2986
2987             self.default_filedir = filedir
2988             paths = filter(None, map(self.filedir_lookup, paths))
2989
2990         result = None
2991         for dir in paths:
2992             verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
2993             node, d = dir.srcdir_find_file(filename)
2994             if node:
2995                 verbose("... FOUND '%s' in '%s'\n" % (filename, d))
2996                 result = node
2997                 break
2998
2999         memo_dict[memo_key] = result
3000
3001         return result
3002
3003 find_file = FileFinder().find_file
3004
3005
3006 def invalidate_node_memos(targets):
3007     """
3008     Invalidate the memoized values of all Nodes (files or directories)
3009     that are associated with the given entries. Has been added to
3010     clear the cache of nodes affected by a direct execution of an
3011     action (e.g.  Delete/Copy/Chmod). Existing Node caches become
3012     inconsistent if the action is run through Execute().  The argument
3013     `targets` can be a single Node object or filename, or a sequence
3014     of Nodes/filenames.
3015     """
3016     from traceback import extract_stack
3017
3018     # First check if the cache really needs to be flushed. Only
3019     # actions run in the SConscript with Execute() seem to be
3020     # affected. XXX The way to check if Execute() is in the stacktrace
3021     # is a very dirty hack and should be replaced by a more sensible
3022     # solution.
3023     must_invalidate = 0
3024     tb = extract_stack()
3025     for f in tb:
3026         if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
3027             must_invalidate = 1
3028     if not must_invalidate:
3029         return
3030
3031     if not SCons.Util.is_List(targets):
3032         targets = [targets]
3033     
3034     for entry in targets:
3035         # If the target is a Node object, clear the cache. If it is a
3036         # filename, look up potentially existing Node object first.
3037         try:
3038             entry.clear_memoized_values()
3039         except AttributeError:
3040             # Not a Node object, try to look up Node by filename.  XXX
3041             # This creates Node objects even for those filenames which
3042             # do not correspond to an existing Node object.
3043             node = get_default_fs().Entry(entry)
3044             if node:
3045                 node.clear_memoized_values()                        
3046