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