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