Merged revisions 2136-2200,2202-2290,2292-2301 via svnmerge from
[scons.git] / src / engine / SCons / Node / FS.py
1 """scons.Node.FS
2
3 File system nodes.
4
5 These Nodes represent the canonical external objects that people think
6 of when they think of building software: files and directories.
7
8 This holds a "default_fs" variable that should be initialized with an FS
9 that can be used by scripts or modules looking for the canonical default.
10
11 """
12
13 #
14 # __COPYRIGHT__
15 #
16 # Permission is hereby granted, free of charge, to any person obtaining
17 # a copy of this software and associated documentation files (the
18 # "Software"), to deal in the Software without restriction, including
19 # without limitation the rights to use, copy, modify, merge, publish,
20 # distribute, sublicense, and/or sell copies of the Software, and to
21 # permit persons to whom the Software is furnished to do so, subject to
22 # the following conditions:
23 #
24 # The above copyright notice and this permission notice shall be included
25 # in all copies or substantial portions of the Software.
26 #
27 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
28 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
29 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 #
35
36 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
37
38 import os
39 import os.path
40 import shutil
41 import stat
42 import string
43 import sys
44 import time
45 import cStringIO
46
47 import SCons.Action
48 from SCons.Debug import logInstanceCreation
49 import SCons.Errors
50 import SCons.Memoize
51 import SCons.Node
52 import SCons.Subst
53 import SCons.Util
54 import SCons.Warnings
55
56 # The max_drift value:  by default, use a cached signature value for
57 # any file that's been untouched for more than two days.
58 default_max_drift = 2*24*60*60
59
60 #
61 # We stringify these file system Nodes a lot.  Turning a file system Node
62 # into a string is non-trivial, because the final string representation
63 # can depend on a lot of factors:  whether it's a derived target or not,
64 # whether it's linked to a repository or source directory, and whether
65 # there's duplication going on.  The normal technique for optimizing
66 # calculations like this is to memoize (cache) the string value, so you
67 # only have to do the calculation once.
68 #
69 # A number of the above factors, however, can be set after we've already
70 # been asked to return a string for a Node, because a Repository() or
71 # BuildDir() call or the like may not occur until later in SConscript
72 # files.  So this variable controls whether we bother trying to save
73 # string values for Nodes.  The wrapper interface can set this whenever
74 # they're done mucking with Repository and BuildDir and the other stuff,
75 # to let this module know it can start returning saved string values
76 # for Nodes.
77 #
78 Save_Strings = None
79
80 def save_strings(val):
81     global Save_Strings
82     Save_Strings = val
83
84 #
85 # SCons.Action objects for interacting with the outside world.
86 #
87 # The Node.FS methods in this module should use these actions to
88 # create and/or remove files and directories; they should *not* use
89 # os.{link,symlink,unlink,mkdir}(), etc., directly.
90 #
91 # Using these SCons.Action objects ensures that descriptions of these
92 # external activities are properly displayed, that the displays are
93 # suppressed when the -s (silent) option is used, and (most importantly)
94 # the actions are disabled when the the -n option is used, in which case
95 # there should be *no* changes to the external file system(s)...
96 #
97
98 if hasattr(os, 'link'):
99     def _hardlink_func(fs, src, dst):
100         # If the source is a symlink, we can't just hard-link to it
101         # because a relative symlink may point somewhere completely
102         # different.  We must disambiguate the symlink and then
103         # hard-link the final destination file.
104         while fs.islink(src):
105             link = fs.readlink(src)
106             if not os.path.isabs(link):
107                 src = link
108             else:
109                 src = os.path.join(os.path.dirname(src), link)
110         fs.link(src, dst)
111 else:
112     _hardlink_func = None
113
114 if hasattr(os, 'symlink'):
115     def _softlink_func(fs, src, dst):
116         fs.symlink(src, dst)
117 else:
118     _softlink_func = None
119
120 def _copy_func(fs, src, dest):
121     shutil.copy2(src, dest)
122     st = fs.stat(src)
123     fs.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
124
125
126 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
127                     'hard-copy', 'soft-copy', 'copy']
128
129 Link_Funcs = [] # contains the callables of the specified duplication style
130
131 def set_duplicate(duplicate):
132     # Fill in the Link_Funcs list according to the argument
133     # (discarding those not available on the platform).
134
135     # Set up the dictionary that maps the argument names to the
136     # underlying implementations.  We do this inside this function,
137     # not in the top-level module code, so that we can remap os.link
138     # and os.symlink for testing purposes.
139     link_dict = {
140         'hard' : _hardlink_func,
141         'soft' : _softlink_func,
142         'copy' : _copy_func
143     }
144
145     if not duplicate in Valid_Duplicates:
146         raise SCons.Errors.InternalError, ("The argument of set_duplicate "
147                                            "should be in Valid_Duplicates")
148     global Link_Funcs
149     Link_Funcs = []
150     for func in string.split(duplicate,'-'):
151         if link_dict[func]:
152             Link_Funcs.append(link_dict[func])
153
154 def LinkFunc(target, source, env):
155     # Relative paths cause problems with symbolic links, so
156     # we use absolute paths, which may be a problem for people
157     # who want to move their soft-linked src-trees around. Those
158     # people should use the 'hard-copy' mode, softlinks cannot be
159     # used for that; at least I have no idea how ...
160     src = source[0].abspath
161     dest = target[0].abspath
162     dir, file = os.path.split(dest)
163     if dir and not target[0].fs.isdir(dir):
164         os.makedirs(dir)
165     if not Link_Funcs:
166         # Set a default order of link functions.
167         set_duplicate('hard-soft-copy')
168     fs = source[0].fs
169     # Now link the files with the previously specified order.
170     for func in Link_Funcs:
171         try:
172             func(fs, src, dest)
173             break
174         except (IOError, OSError):
175             # An OSError indicates something happened like a permissions
176             # problem or an attempt to symlink across file-system
177             # boundaries.  An IOError indicates something like the file
178             # not existing.  In either case, keeping trying additional
179             # functions in the list and only raise an error if the last
180             # one failed.
181             if func == Link_Funcs[-1]:
182                 # exception of the last link method (copy) are fatal
183                 raise
184             else:
185                 pass
186     return 0
187
188 Link = SCons.Action.Action(LinkFunc, None)
189 def LocalString(target, source, env):
190     return 'Local copy of %s from %s' % (target[0], source[0])
191
192 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
193
194 def UnlinkFunc(target, source, env):
195     t = target[0]
196     t.fs.unlink(t.abspath)
197     return 0
198
199 Unlink = SCons.Action.Action(UnlinkFunc, None)
200
201 def MkdirFunc(target, source, env):
202     t = target[0]
203     if not t.exists():
204         t.fs.mkdir(t.abspath)
205     return 0
206
207 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
208
209 MkdirBuilder = None
210
211 def get_MkdirBuilder():
212     global MkdirBuilder
213     if MkdirBuilder is None:
214         import SCons.Builder
215         import SCons.Defaults
216         # "env" will get filled in by Executor.get_build_env()
217         # calling SCons.Defaults.DefaultEnvironment() when necessary.
218         MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
219                                              env = None,
220                                              explain = None,
221                                              is_explicit = None,
222                                              target_scanner = SCons.Defaults.DirEntryScanner,
223                                              name = "MkdirBuilder")
224     return MkdirBuilder
225
226 class _Null:
227     pass
228
229 _null = _Null()
230
231 DefaultSCCSBuilder = None
232 DefaultRCSBuilder = None
233
234 def get_DefaultSCCSBuilder():
235     global DefaultSCCSBuilder
236     if DefaultSCCSBuilder is None:
237         import SCons.Builder
238         # "env" will get filled in by Executor.get_build_env()
239         # calling SCons.Defaults.DefaultEnvironment() when necessary.
240         act = SCons.Action.Action('$SCCSCOM', '$SCCSCOMSTR')
241         DefaultSCCSBuilder = SCons.Builder.Builder(action = act,
242                                                    env = None,
243                                                    name = "DefaultSCCSBuilder")
244     return DefaultSCCSBuilder
245
246 def get_DefaultRCSBuilder():
247     global DefaultRCSBuilder
248     if DefaultRCSBuilder is None:
249         import SCons.Builder
250         # "env" will get filled in by Executor.get_build_env()
251         # calling SCons.Defaults.DefaultEnvironment() when necessary.
252         act = SCons.Action.Action('$RCS_COCOM', '$RCS_COCOMSTR')
253         DefaultRCSBuilder = SCons.Builder.Builder(action = act,
254                                                   env = None,
255                                                   name = "DefaultRCSBuilder")
256     return DefaultRCSBuilder
257
258 # Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
259 _is_cygwin = sys.platform == "cygwin"
260 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
261     def _my_normcase(x):
262         return x
263 else:
264     def _my_normcase(x):
265         return string.upper(x)
266
267
268
269 class DiskChecker:
270     def __init__(self, type, do, ignore):
271         self.type = type
272         self.do = do
273         self.ignore = ignore
274         self.set_do()
275     def set_do(self):
276         self.__call__ = self.do
277     def set_ignore(self):
278         self.__call__ = self.ignore
279     def set(self, list):
280         if self.type in list:
281             self.set_do()
282         else:
283             self.set_ignore()
284
285 def do_diskcheck_match(node, predicate, errorfmt):
286     result = predicate()
287     try:
288         # If calling the predicate() cached a None value from stat(),
289         # remove it so it doesn't interfere with later attempts to
290         # build this Node as we walk the DAG.  (This isn't a great way
291         # to do this, we're reaching into an interface that doesn't
292         # really belong to us, but it's all about performance, so
293         # for now we'll just document the dependency...)
294         if node._memo['stat'] is None:
295             del node._memo['stat']
296     except (AttributeError, KeyError):
297         pass
298     if result:
299         raise TypeError, errorfmt % node.abspath
300
301 def ignore_diskcheck_match(node, predicate, errorfmt):
302     pass
303
304 def do_diskcheck_rcs(node, name):
305     try:
306         rcs_dir = node.rcs_dir
307     except AttributeError:
308         if node.entry_exists_on_disk('RCS'):
309             rcs_dir = node.Dir('RCS')
310         else:
311             rcs_dir = None
312         node.rcs_dir = rcs_dir
313     if rcs_dir:
314         return rcs_dir.entry_exists_on_disk(name+',v')
315     return None
316
317 def ignore_diskcheck_rcs(node, name):
318     return None
319
320 def do_diskcheck_sccs(node, name):
321     try:
322         sccs_dir = node.sccs_dir
323     except AttributeError:
324         if node.entry_exists_on_disk('SCCS'):
325             sccs_dir = node.Dir('SCCS')
326         else:
327             sccs_dir = None
328         node.sccs_dir = sccs_dir
329     if sccs_dir:
330         return sccs_dir.entry_exists_on_disk('s.'+name)
331     return None
332
333 def ignore_diskcheck_sccs(node, name):
334     return None
335
336 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
337 diskcheck_rcs = DiskChecker('rcs', do_diskcheck_rcs, ignore_diskcheck_rcs)
338 diskcheck_sccs = DiskChecker('sccs', do_diskcheck_sccs, ignore_diskcheck_sccs)
339
340 diskcheckers = [
341     diskcheck_match,
342     diskcheck_rcs,
343     diskcheck_sccs,
344 ]
345
346 def set_diskcheck(list):
347     for dc in diskcheckers:
348         dc.set(list)
349
350 def diskcheck_types():
351     return map(lambda dc: dc.type, diskcheckers)
352
353
354
355 class EntryProxy(SCons.Util.Proxy):
356     def __get_abspath(self):
357         entry = self.get()
358         return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
359                                              entry.name + "_abspath")
360
361     def __get_filebase(self):
362         name = self.get().name
363         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
364                                              name + "_filebase")
365
366     def __get_suffix(self):
367         name = self.get().name
368         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
369                                              name + "_suffix")
370
371     def __get_file(self):
372         name = self.get().name
373         return SCons.Subst.SpecialAttrWrapper(name, name + "_file")
374
375     def __get_base_path(self):
376         """Return the file's directory and file name, with the
377         suffix stripped."""
378         entry = self.get()
379         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
380                                              entry.name + "_base")
381
382     def __get_posix_path(self):
383         """Return the path with / as the path separator,
384         regardless of platform."""
385         if os.sep == '/':
386             return self
387         else:
388             entry = self.get()
389             r = string.replace(entry.get_path(), os.sep, '/')
390             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
391
392     def __get_windows_path(self):
393         """Return the path with \ as the path separator,
394         regardless of platform."""
395         if os.sep == '\\':
396             return self
397         else:
398             entry = self.get()
399             r = string.replace(entry.get_path(), os.sep, '\\')
400             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
401
402     def __get_srcnode(self):
403         return EntryProxy(self.get().srcnode())
404
405     def __get_srcdir(self):
406         """Returns the directory containing the source node linked to this
407         node via BuildDir(), or the directory of this node if not linked."""
408         return EntryProxy(self.get().srcnode().dir)
409
410     def __get_rsrcnode(self):
411         return EntryProxy(self.get().srcnode().rfile())
412
413     def __get_rsrcdir(self):
414         """Returns the directory containing the source node linked to this
415         node via BuildDir(), or the directory of this node if not linked."""
416         return EntryProxy(self.get().srcnode().rfile().dir)
417
418     def __get_dir(self):
419         return EntryProxy(self.get().dir)
420
421     dictSpecialAttrs = { "base"     : __get_base_path,
422                          "posix"    : __get_posix_path,
423                          "windows"  : __get_windows_path,
424                          "win32"    : __get_windows_path,
425                          "srcpath"  : __get_srcnode,
426                          "srcdir"   : __get_srcdir,
427                          "dir"      : __get_dir,
428                          "abspath"  : __get_abspath,
429                          "filebase" : __get_filebase,
430                          "suffix"   : __get_suffix,
431                          "file"     : __get_file,
432                          "rsrcpath" : __get_rsrcnode,
433                          "rsrcdir"  : __get_rsrcdir,
434                        }
435
436     def __getattr__(self, name):
437         # This is how we implement the "special" attributes
438         # such as base, posix, srcdir, etc.
439         try:
440             attr_function = self.dictSpecialAttrs[name]
441         except KeyError:
442             try:
443                 attr = SCons.Util.Proxy.__getattr__(self, name)
444             except AttributeError:
445                 entry = self.get()
446                 classname = string.split(str(entry.__class__), '.')[-1]
447                 if classname[-2:] == "'>":
448                     # new-style classes report their name as:
449                     #   "<class 'something'>"
450                     # instead of the classic classes:
451                     #   "something"
452                     classname = classname[:-2]
453                 raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
454             return attr
455         else:
456             return attr_function(self)
457
458 class Base(SCons.Node.Node):
459     """A generic class for file system entries.  This class is for
460     when we don't know yet whether the entry being looked up is a file
461     or a directory.  Instances of this class can morph into either
462     Dir or File objects by a later, more precise lookup.
463
464     Note: this class does not define __cmp__ and __hash__ for
465     efficiency reasons.  SCons does a lot of comparing of
466     Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
467     as fast as possible, which means we want to use Python's built-in
468     object identity comparisons.
469     """
470
471     memoizer_counters = []
472
473     def __init__(self, name, directory, fs):
474         """Initialize a generic Node.FS.Base object.
475
476         Call the superclass initialization, take care of setting up
477         our relative and absolute paths, identify our parent
478         directory, and indicate that this node should use
479         signatures."""
480         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
481         SCons.Node.Node.__init__(self)
482
483         self.name = name
484         self.suffix = SCons.Util.splitext(name)[1]
485         self.fs = fs
486
487         assert directory, "A directory must be provided"
488
489         self.abspath = directory.entry_abspath(name)
490         if directory.path == '.':
491             self.path = name
492         else:
493             self.path = directory.entry_path(name)
494         if directory.tpath == '.':
495             self.tpath = name
496         else:
497             self.tpath = directory.entry_tpath(name)
498         self.path_elements = directory.path_elements + [self]
499
500         self.dir = directory
501         self.cwd = None # will hold the SConscript directory for target nodes
502         self.duplicate = directory.duplicate
503
504     def must_be_same(self, klass):
505         """
506         This node, which already existed, is being looked up as the
507         specified klass.  Raise an exception if it isn't.
508         """
509         if self.__class__ is klass or klass is Entry:
510             return
511         raise TypeError, "Tried to lookup %s '%s' as a %s." %\
512               (self.__class__.__name__, self.path, klass.__name__)
513
514     def get_dir(self):
515         return self.dir
516
517     def get_suffix(self):
518         return self.suffix
519
520     def rfile(self):
521         return self
522
523     def __str__(self):
524         """A Node.FS.Base object's string representation is its path
525         name."""
526         global Save_Strings
527         if Save_Strings:
528             return self._save_str()
529         return self._get_str()
530
531     memoizer_counters.append(SCons.Memoize.CountValue('_save_str'))
532
533     def _save_str(self):
534         try:
535             return self._memo['_save_str']
536         except KeyError:
537             pass
538         result = self._get_str()
539         self._memo['_save_str'] = result
540         return result
541
542     def _get_str(self):
543         if self.duplicate or self.is_derived():
544             return self.get_path()
545         return self.srcnode().get_path()
546
547     rstr = __str__
548
549     memoizer_counters.append(SCons.Memoize.CountValue('stat'))
550
551     def stat(self):
552         try: return self._memo['stat']
553         except KeyError: pass
554         try: result = self.fs.stat(self.abspath)
555         except os.error: result = None
556         self._memo['stat'] = result
557         return result
558
559     def exists(self):
560         return not self.stat() is None
561
562     def rexists(self):
563         return self.rfile().exists()
564
565     def getmtime(self):
566         st = self.stat()
567         if st: return st[stat.ST_MTIME]
568         else: return None
569
570     def getsize(self):
571         st = self.stat()
572         if st: return st[stat.ST_SIZE]
573         else: return None
574
575     def isdir(self):
576         st = self.stat()
577         return not st is None and stat.S_ISDIR(st[stat.ST_MODE])
578
579     def isfile(self):
580         st = self.stat()
581         return not st is None and stat.S_ISREG(st[stat.ST_MODE])
582
583     if hasattr(os, 'symlink'):
584         def islink(self):
585             try: st = self.fs.lstat(self.abspath)
586             except os.error: return 0
587             return stat.S_ISLNK(st[stat.ST_MODE])
588     else:
589         def islink(self):
590             return 0                    # no symlinks
591
592     def is_under(self, dir):
593         if self is dir:
594             return 1
595         else:
596             return self.dir.is_under(dir)
597
598     def set_local(self):
599         self._local = 1
600
601     def srcnode(self):
602         """If this node is in a build path, return the node
603         corresponding to its source file.  Otherwise, return
604         ourself.
605         """
606         dir=self.dir
607         name=self.name
608         while dir:
609             if dir.srcdir:
610                 srcnode = dir.srcdir.Entry(name)
611                 srcnode.must_be_same(self.__class__)
612                 return srcnode
613             name = dir.name + os.sep + name
614             dir = dir.up()
615         return self
616
617     def get_path(self, dir=None):
618         """Return path relative to the current working directory of the
619         Node.FS.Base object that owns us."""
620         if not dir:
621             dir = self.fs.getcwd()
622         if self == dir:
623             return '.'
624         path_elems = self.path_elements
625         try: i = path_elems.index(dir)
626         except ValueError: pass
627         else: path_elems = path_elems[i+1:]
628         path_elems = map(lambda n: n.name, path_elems)
629         return string.join(path_elems, os.sep)
630
631     def set_src_builder(self, builder):
632         """Set the source code builder for this node."""
633         self.sbuilder = builder
634         if not self.has_builder():
635             self.builder_set(builder)
636
637     def src_builder(self):
638         """Fetch the source code builder for this node.
639
640         If there isn't one, we cache the source code builder specified
641         for the directory (which in turn will cache the value from its
642         parent directory, and so on up to the file system root).
643         """
644         try:
645             scb = self.sbuilder
646         except AttributeError:
647             scb = self.dir.src_builder()
648             self.sbuilder = scb
649         return scb
650
651     def get_abspath(self):
652         """Get the absolute path of the file."""
653         return self.abspath
654
655     def for_signature(self):
656         # Return just our name.  Even an absolute path would not work,
657         # because that can change thanks to symlinks or remapped network
658         # paths.
659         return self.name
660
661     def get_subst_proxy(self):
662         try:
663             return self._proxy
664         except AttributeError:
665             ret = EntryProxy(self)
666             self._proxy = ret
667             return ret
668
669     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
670         """
671
672         Generates a target entry that corresponds to this entry (usually
673         a source file) with the specified prefix and suffix.
674
675         Note that this method can be overridden dynamically for generated
676         files that need different behavior.  See Tool/swig.py for
677         an example.
678         """
679         return self.dir.Entry(prefix + splitext(self.name)[0] + suffix)
680
681     def _Rfindalldirs_key(self, pathlist):
682         return pathlist
683
684     memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key))
685
686     def Rfindalldirs(self, pathlist):
687         """
688         Return all of the directories for a given path list, including
689         corresponding "backing" directories in any repositories.
690
691         The Node lookups are relative to this Node (typically a
692         directory), so memoizing result saves cycles from looking
693         up the same path for each target in a given directory.
694         """
695         try:
696             memo_dict = self._memo['Rfindalldirs']
697         except KeyError:
698             memo_dict = {}
699             self._memo['Rfindalldirs'] = memo_dict
700         else:
701             try:
702                 return memo_dict[pathlist]
703             except KeyError:
704                 pass
705
706         create_dir_relative_to_self = self.Dir
707         result = []
708         for path in pathlist:
709             if isinstance(path, SCons.Node.Node):
710                 result.append(path)
711             else:
712                 dir = create_dir_relative_to_self(path)
713                 result.extend(dir.get_all_rdirs())
714
715         memo_dict[pathlist] = result
716
717         return result
718
719     def RDirs(self, pathlist):
720         """Search for a list of directories in the Repository list."""
721         cwd = self.cwd or self.fs._cwd
722         return cwd.Rfindalldirs(pathlist)
723
724     memoizer_counters.append(SCons.Memoize.CountValue('rentry'))
725
726     def rentry(self):
727         try:
728             return self._memo['rentry']
729         except KeyError:
730             pass
731         result = self
732         if not self.exists():
733             norm_name = _my_normcase(self.name)
734             for dir in self.dir.get_all_rdirs():
735                 try:
736                     node = dir.entries[norm_name]
737                 except KeyError:
738                     if dir.entry_exists_on_disk(self.name):
739                         result = dir.Entry(self.name)
740                         break
741         self._memo['rentry'] = result
742         return result
743
744 class Entry(Base):
745     """This is the class for generic Node.FS entries--that is, things
746     that could be a File or a Dir, but we're just not sure yet.
747     Consequently, the methods in this class really exist just to
748     transform their associated object into the right class when the
749     time comes, and then call the same-named method in the transformed
750     class."""
751
752     def diskcheck_match(self):
753         pass
754
755     def disambiguate(self, must_exist=None):
756         """
757         """
758         if self.isdir():
759             self.__class__ = Dir
760             self._morph()
761         elif self.isfile():
762             self.__class__ = File
763             self._morph()
764             self.clear()
765         else:
766             # There was nothing on-disk at this location, so look in
767             # the src directory.
768             #
769             # We can't just use self.srcnode() straight away because
770             # that would create an actual Node for this file in the src
771             # directory, and there might not be one.  Instead, use the
772             # dir_on_disk() method to see if there's something on-disk
773             # with that name, in which case we can go ahead and call
774             # self.srcnode() to create the right type of entry.
775             srcdir = self.dir.srcnode()
776             if srcdir != self.dir and \
777                srcdir.entry_exists_on_disk(self.name) and \
778                self.srcnode().isdir():
779                 self.__class__ = Dir
780                 self._morph()
781             elif must_exist:
782                 msg = "No such file or directory: '%s'" % self.abspath
783                 raise SCons.Errors.UserError, msg
784             else:
785                 self.__class__ = File
786                 self._morph()
787                 self.clear()
788         return self
789
790     def rfile(self):
791         """We're a generic Entry, but the caller is actually looking for
792         a File at this point, so morph into one."""
793         self.__class__ = File
794         self._morph()
795         self.clear()
796         return File.rfile(self)
797
798     def scanner_key(self):
799         return self.get_suffix()
800
801     def get_contents(self):
802         """Fetch the contents of the entry.
803
804         Since this should return the real contents from the file
805         system, we check to see into what sort of subclass we should
806         morph this Entry."""
807         try:
808             self = self.disambiguate(must_exist=1)
809         except SCons.Errors.UserError:
810             # There was nothing on disk with which to disambiguate
811             # this entry.  Leave it as an Entry, but return a null
812             # string so calls to get_contents() in emitters and the
813             # like (e.g. in qt.py) don't have to disambiguate by hand
814             # or catch the exception.
815             return ''
816         else:
817             return self.get_contents()
818
819     def must_be_same(self, klass):
820         """Called to make sure a Node is a Dir.  Since we're an
821         Entry, we can morph into one."""
822         if not self.__class__ is klass:
823             self.__class__ = klass
824             self._morph()
825             self.clear
826
827     # The following methods can get called before the Taskmaster has
828     # had a chance to call disambiguate() directly to see if this Entry
829     # should really be a Dir or a File.  We therefore use these to call
830     # disambiguate() transparently (from our caller's point of view).
831     #
832     # Right now, this minimal set of methods has been derived by just
833     # looking at some of the methods that will obviously be called early
834     # in any of the various Taskmasters' calling sequences, and then
835     # empirically figuring out which additional methods are necessary
836     # to make various tests pass.
837
838     def exists(self):
839         """Return if the Entry exists.  Check the file system to see
840         what we should turn into first.  Assume a file if there's no
841         directory."""
842         return self.disambiguate().exists()
843
844     def rel_path(self, other):
845         d = self.disambiguate()
846         if d.__class__ == Entry:
847             raise "rel_path() could not disambiguate File/Dir"
848         return d.rel_path(other)
849
850 # This is for later so we can differentiate between Entry the class and Entry
851 # the method of the FS class.
852 _classEntry = Entry
853
854
855 class LocalFS:
856
857     if SCons.Memoize.use_memoizer:
858         __metaclass__ = SCons.Memoize.Memoized_Metaclass
859
860     # This class implements an abstraction layer for operations involving
861     # a local file system.  Essentially, this wraps any function in
862     # the os, os.path or shutil modules that we use to actually go do
863     # anything with or to the local file system.
864     #
865     # Note that there's a very good chance we'll refactor this part of
866     # the architecture in some way as we really implement the interface(s)
867     # for remote file system Nodes.  For example, the right architecture
868     # might be to have this be a subclass instead of a base class.
869     # Nevertheless, we're using this as a first step in that direction.
870     #
871     # We're not using chdir() yet because the calling subclass method
872     # needs to use os.chdir() directly to avoid recursion.  Will we
873     # really need this one?
874     #def chdir(self, path):
875     #    return os.chdir(path)
876     def chmod(self, path, mode):
877         return os.chmod(path, mode)
878     def copy2(self, src, dst):
879         return shutil.copy2(src, dst)
880     def exists(self, path):
881         return os.path.exists(path)
882     def getmtime(self, path):
883         return os.path.getmtime(path)
884     def getsize(self, path):
885         return os.path.getsize(path)
886     def isdir(self, path):
887         return os.path.isdir(path)
888     def isfile(self, path):
889         return os.path.isfile(path)
890     def link(self, src, dst):
891         return os.link(src, dst)
892     def lstat(self, path):
893         return os.lstat(path)
894     def listdir(self, path):
895         return os.listdir(path)
896     def makedirs(self, path):
897         return os.makedirs(path)
898     def mkdir(self, path):
899         return os.mkdir(path)
900     def rename(self, old, new):
901         return os.rename(old, new)
902     def stat(self, path):
903         return os.stat(path)
904     def symlink(self, src, dst):
905         return os.symlink(src, dst)
906     def open(self, path):
907         return open(path)
908     def unlink(self, path):
909         return os.unlink(path)
910
911     if hasattr(os, 'symlink'):
912         def islink(self, path):
913             return os.path.islink(path)
914     else:
915         def islink(self, path):
916             return 0                    # no symlinks
917
918     if hasattr(os, 'readlink'):
919         def readlink(self, file):
920             return os.readlink(file)
921     else:
922         def readlink(self, file):
923             return ''
924
925
926 #class RemoteFS:
927 #    # Skeleton for the obvious methods we might need from the
928 #    # abstraction layer for a remote filesystem.
929 #    def upload(self, local_src, remote_dst):
930 #        pass
931 #    def download(self, remote_src, local_dst):
932 #        pass
933
934
935 class FS(LocalFS):
936
937     memoizer_counters = []
938
939     def __init__(self, path = None):
940         """Initialize the Node.FS subsystem.
941
942         The supplied path is the top of the source tree, where we
943         expect to find the top-level build file.  If no path is
944         supplied, the current directory is the default.
945
946         The path argument must be a valid absolute path.
947         """
948         if __debug__: logInstanceCreation(self, 'Node.FS')
949
950         self._memo = {}
951
952         self.Root = {}
953         self.SConstruct_dir = None
954         self.max_drift = default_max_drift
955
956         self.Top = None
957         if path is None:
958             self.pathTop = os.getcwd()
959         else:
960             self.pathTop = path
961         self.defaultDrive = _my_normcase(os.path.splitdrive(self.pathTop)[0])
962
963         self.Top = self._doLookup(Dir, os.path.normpath(self.pathTop))
964         self.Top.path = '.'
965         self.Top.tpath = '.'
966         self._cwd = self.Top
967
968     def set_SConstruct_dir(self, dir):
969         self.SConstruct_dir = dir
970
971     def get_max_drift(self):
972         return self.max_drift
973
974     def set_max_drift(self, max_drift):
975         self.max_drift = max_drift
976
977     def getcwd(self):
978         return self._cwd
979
980     def _doLookup_key(self, fsclass, name, directory = None, create = 1):
981         return (fsclass, name, directory)
982
983     memoizer_counters.append(SCons.Memoize.CountDict('_doLookup', _doLookup_key))
984
985     def _doLookup(self, fsclass, name, directory = None, create = 1):
986         """This method differs from the File and Dir factory methods in
987         one important way: the meaning of the directory parameter.
988         In this method, if directory is None or not supplied, the supplied
989         name is expected to be an absolute path.  If you try to look up a
990         relative path with directory=None, then an AssertionError will be
991         raised.
992         """
993         memo_key = (fsclass, name, directory)
994         try:
995             memo_dict = self._memo['_doLookup']
996         except KeyError:
997             memo_dict = {}
998             self._memo['_doLookup'] = memo_dict
999         else:
1000             try:
1001                 return memo_dict[memo_key]
1002             except KeyError:
1003                 pass
1004
1005         if not name:
1006             # This is a stupid hack to compensate for the fact that the
1007             # POSIX and Windows versions of os.path.normpath() behave
1008             # differently in older versions of Python.  In particular,
1009             # in POSIX:
1010             #   os.path.normpath('./') == '.'
1011             # in Windows:
1012             #   os.path.normpath('./') == ''
1013             #   os.path.normpath('.\\') == ''
1014             #
1015             # This is a definite bug in the Python library, but we have
1016             # to live with it.
1017             name = '.'
1018         path_orig = string.split(name, os.sep)
1019         path_norm = string.split(_my_normcase(name), os.sep)
1020
1021         first_orig = path_orig.pop(0)   # strip first element
1022         unused = path_norm.pop(0)   # strip first element
1023
1024         drive, path_first = os.path.splitdrive(first_orig)
1025         if path_first:
1026             path_orig = [ path_first, ] + path_orig
1027             path_norm = [ _my_normcase(path_first), ] + path_norm
1028         else:
1029             # Absolute path
1030             drive = _my_normcase(drive)
1031             try:
1032                 directory = self.Root[drive]
1033             except KeyError:
1034                 if not create:
1035                     raise SCons.Errors.UserError
1036                 directory = RootDir(drive, self)
1037                 self.Root[drive] = directory
1038                 if not drive:
1039                     self.Root[self.defaultDrive] = directory
1040                 elif drive == self.defaultDrive:
1041                     self.Root[''] = directory
1042
1043         if not path_orig:
1044             memo_dict[memo_key] = directory
1045             return directory
1046
1047         last_orig = path_orig.pop()     # strip last element
1048         last_norm = path_norm.pop()     # strip last element
1049
1050         # Lookup the directory
1051         for orig, norm in map(None, path_orig, path_norm):
1052             try:
1053                 entries = directory.entries
1054             except AttributeError:
1055                 # We tried to look up the entry in either an Entry or
1056                 # a File.  Give whatever it is a chance to do what's
1057                 # appropriate: morph into a Dir or raise an exception.
1058                 directory.must_be_same(Dir)
1059                 entries = directory.entries
1060             try:
1061                 directory = entries[norm]
1062             except KeyError:
1063                 if not create:
1064                     raise SCons.Errors.UserError
1065
1066                 d = Dir(orig, directory, self)
1067
1068                 # Check the file system (or not, as configured) to make
1069                 # sure there isn't already a file there.
1070                 d.diskcheck_match()
1071
1072                 directory.entries[norm] = d
1073                 directory.add_wkid(d)
1074                 directory = d
1075
1076         directory.must_be_same(Dir)
1077
1078         try:
1079             e = directory.entries[last_norm]
1080         except KeyError:
1081             if not create:
1082                 raise SCons.Errors.UserError
1083
1084             result = fsclass(last_orig, directory, self)
1085
1086             # Check the file system (or not, as configured) to make
1087             # sure there isn't already a directory at the path on
1088             # disk where we just created a File node, and vice versa.
1089             result.diskcheck_match()
1090
1091             directory.entries[last_norm] = result
1092             directory.add_wkid(result)
1093         else:
1094             e.must_be_same(fsclass)
1095             result = e
1096
1097         memo_dict[memo_key] = result
1098
1099         return result 
1100
1101     def _transformPath(self, name, directory):
1102         """Take care of setting up the correct top-level directory,
1103         usually in preparation for a call to doLookup().
1104
1105         If the path name is prepended with a '#', then it is unconditionally
1106         interpreted as relative to the top-level directory of this FS.
1107
1108         If directory is None, and name is a relative path,
1109         then the same applies.
1110         """
1111         try:
1112             # Decide if this is a top-relative look up.  The normal case
1113             # (by far) is handed a non-zero-length string to look up,
1114             # so just (try to) check for the initial '#'.
1115             top_relative = (name[0] == '#')
1116         except (AttributeError, IndexError):
1117             # The exceptions we may encounter in unusual cases:
1118             #   AttributeError: a proxy without a __getitem__() method.
1119             #   IndexError: a null string.
1120             top_relative = False
1121             name = str(name)
1122         if top_relative:
1123             directory = self.Top
1124             name = name[1:]
1125             if name and (name[0] == os.sep or name[0] == '/'):
1126                 # Correct such that '#/foo' is equivalent
1127                 # to '#foo'.
1128                 name = name[1:]
1129             name = os.path.normpath(os.path.join('.', name))
1130             return (name, directory)
1131         elif not directory:
1132             directory = self._cwd
1133         return (os.path.normpath(name), directory)
1134
1135     def chdir(self, dir, change_os_dir=0):
1136         """Change the current working directory for lookups.
1137         If change_os_dir is true, we will also change the "real" cwd
1138         to match.
1139         """
1140         curr=self._cwd
1141         try:
1142             if not dir is None:
1143                 self._cwd = dir
1144                 if change_os_dir:
1145                     os.chdir(dir.abspath)
1146         except OSError:
1147             self._cwd = curr
1148             raise
1149
1150     def Entry(self, name, directory = None, create = 1, klass=None):
1151         """Lookup or create a generic Entry node with the specified name.
1152         If the name is a relative path (begins with ./, ../, or a file
1153         name), then it is looked up relative to the supplied directory
1154         node, or to the top level directory of the FS (supplied at
1155         construction time) if no directory is supplied.
1156         """
1157
1158         if not klass:
1159             klass = Entry
1160
1161         if isinstance(name, Base):
1162             name.must_be_same(klass)
1163             return name
1164         else:
1165             if directory and not isinstance(directory, Dir):
1166                 directory = self.Dir(directory)
1167             name, directory = self._transformPath(name, directory)
1168             return self._doLookup(klass, name, directory, create)
1169
1170     def File(self, name, directory = None, create = 1):
1171         """Lookup or create a File node with the specified name.  If
1172         the name is a relative path (begins with ./, ../, or a file name),
1173         then it is looked up relative to the supplied directory node,
1174         or to the top level directory of the FS (supplied at construction
1175         time) if no directory is supplied.
1176
1177         This method will raise TypeError if a directory is found at the
1178         specified path.
1179         """
1180         return self.Entry(name, directory, create, File)
1181
1182     def Dir(self, name, directory = None, create = 1):
1183         """Lookup or create a Dir node with the specified name.  If
1184         the name is a relative path (begins with ./, ../, or a file name),
1185         then it is looked up relative to the supplied directory node,
1186         or to the top level directory of the FS (supplied at construction
1187         time) if no directory is supplied.
1188
1189         This method will raise TypeError if a normal file is found at the
1190         specified path.
1191         """
1192         return self.Entry(name, directory, create, Dir)
1193
1194     def BuildDir(self, build_dir, src_dir, duplicate=1):
1195         """Link the supplied build directory to the source directory
1196         for purposes of building files."""
1197
1198         if not isinstance(src_dir, SCons.Node.Node):
1199             src_dir = self.Dir(src_dir)
1200         if not isinstance(build_dir, SCons.Node.Node):
1201             build_dir = self.Dir(build_dir)
1202         if src_dir.is_under(build_dir):
1203             raise SCons.Errors.UserError, "Source directory cannot be under build directory."
1204         if build_dir.srcdir:
1205             if build_dir.srcdir == src_dir:
1206                 return # We already did this.
1207             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(build_dir, build_dir.srcdir)
1208         build_dir.link(src_dir, duplicate)
1209
1210     def Repository(self, *dirs):
1211         """Specify Repository directories to search."""
1212         for d in dirs:
1213             if not isinstance(d, SCons.Node.Node):
1214                 d = self.Dir(d)
1215             self.Top.addRepository(d)
1216
1217     def build_dir_target_climb(self, orig, dir, tail):
1218         """Create targets in corresponding build directories
1219
1220         Climb the directory tree, and look up path names
1221         relative to any linked build directories we find.
1222
1223         Even though this loops and walks up the tree, we don't memoize
1224         the return value because this is really only used to process
1225         the command-line targets.
1226         """
1227         targets = []
1228         message = None
1229         fmt = "building associated BuildDir targets: %s"
1230         start_dir = dir
1231         while dir:
1232             for bd in dir.build_dirs:
1233                 if start_dir.is_under(bd):
1234                     # If already in the build-dir location, don't reflect
1235                     return [orig], fmt % str(orig)
1236                 p = apply(os.path.join, [bd.path] + tail)
1237                 targets.append(self.Entry(p))
1238             tail = [dir.name] + tail
1239             dir = dir.up()
1240         if targets:
1241             message = fmt % string.join(map(str, targets))
1242         return targets, message
1243
1244 class DirNodeInfo(SCons.Node.NodeInfoBase):
1245     pass
1246
1247 class DirBuildInfo(SCons.Node.BuildInfoBase):
1248     pass
1249
1250 class Dir(Base):
1251     """A class for directories in a file system.
1252     """
1253
1254     memoizer_counters = []
1255
1256     NodeInfo = DirNodeInfo
1257     BuildInfo = DirBuildInfo
1258
1259     def __init__(self, name, directory, fs):
1260         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1261         Base.__init__(self, name, directory, fs)
1262         self._morph()
1263
1264     def _morph(self):
1265         """Turn a file system Node (either a freshly initialized directory
1266         object or a separate Entry object) into a proper directory object.
1267
1268         Set up this directory's entries and hook it into the file
1269         system tree.  Specify that directories (this Node) don't use
1270         signatures for calculating whether they're current.
1271         """
1272
1273         self.repositories = []
1274         self.srcdir = None
1275
1276         self.entries = {}
1277         self.entries['.'] = self
1278         self.entries['..'] = self.dir
1279         self.cwd = self
1280         self.searched = 0
1281         self._sconsign = None
1282         self.build_dirs = []
1283
1284         # Don't just reset the executor, replace its action list,
1285         # because it might have some pre-or post-actions that need to
1286         # be preserved.
1287         self.builder = get_MkdirBuilder()
1288         self.get_executor().set_action_list(self.builder.action)
1289
1290     def diskcheck_match(self):
1291         diskcheck_match(self, self.isfile,
1292                         "File %s found where directory expected.")
1293
1294     def __clearRepositoryCache(self, duplicate=None):
1295         """Called when we change the repository(ies) for a directory.
1296         This clears any cached information that is invalidated by changing
1297         the repository."""
1298
1299         for node in self.entries.values():
1300             if node != self.dir:
1301                 if node != self and isinstance(node, Dir):
1302                     node.__clearRepositoryCache(duplicate)
1303                 else:
1304                     node.clear()
1305                     try:
1306                         del node._srcreps
1307                     except AttributeError:
1308                         pass
1309                     if duplicate != None:
1310                         node.duplicate=duplicate
1311
1312     def __resetDuplicate(self, node):
1313         if node != self:
1314             node.duplicate = node.get_dir().duplicate
1315
1316     def Entry(self, name):
1317         """Create an entry node named 'name' relative to this directory."""
1318         return self.fs.Entry(name, self)
1319
1320     def Dir(self, name):
1321         """Create a directory node named 'name' relative to this directory."""
1322         dir = self.fs.Dir(name, self)
1323         return dir
1324
1325     def File(self, name):
1326         """Create a file node named 'name' relative to this directory."""
1327         return self.fs.File(name, self)
1328
1329     def link(self, srcdir, duplicate):
1330         """Set this directory as the build directory for the
1331         supplied source directory."""
1332         self.srcdir = srcdir
1333         self.duplicate = duplicate
1334         self.__clearRepositoryCache(duplicate)
1335         srcdir.build_dirs.append(self)
1336
1337     def getRepositories(self):
1338         """Returns a list of repositories for this directory.
1339         """
1340         if self.srcdir and not self.duplicate:
1341             return self.srcdir.get_all_rdirs() + self.repositories
1342         return self.repositories
1343
1344     memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
1345
1346     def get_all_rdirs(self):
1347         try:
1348             return self._memo['get_all_rdirs']
1349         except KeyError:
1350             pass
1351
1352         result = [self]
1353         fname = '.'
1354         dir = self
1355         while dir:
1356             for rep in dir.getRepositories():
1357                 result.append(rep.Dir(fname))
1358             fname = dir.name + os.sep + fname
1359             dir = dir.up()
1360
1361         self._memo['get_all_rdirs'] = result
1362
1363         return result
1364
1365     def addRepository(self, dir):
1366         if dir != self and not dir in self.repositories:
1367             self.repositories.append(dir)
1368             dir.tpath = '.'
1369             self.__clearRepositoryCache()
1370
1371     def up(self):
1372         return self.entries['..']
1373
1374     def _rel_path_key(self, other):
1375         return str(other)
1376
1377     memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
1378
1379     def rel_path(self, other):
1380         """Return a path to "other" relative to this directory.
1381         """
1382         try:
1383             memo_dict = self._memo['rel_path']
1384         except KeyError:
1385             memo_dict = {}
1386             self._memo['rel_path'] = memo_dict
1387         else:
1388             try:
1389                 return memo_dict[other]
1390             except KeyError:
1391                 pass
1392
1393         if self is other:
1394
1395             result = '.'
1396
1397         elif not other in self.path_elements:
1398
1399             try:
1400                 other_dir = other.get_dir()
1401             except AttributeError:
1402                 result = str(other)
1403             else:
1404                 if other_dir is None:
1405                     result = other.name
1406                 else:
1407                     dir_rel_path = self.rel_path(other_dir)
1408                     if dir_rel_path == '.':
1409                         result = other.name
1410                     else:
1411                         result = dir_rel_path + os.sep + other.name
1412
1413         else:
1414
1415             i = self.path_elements.index(other) + 1
1416
1417             path_elems = ['..'] * (len(self.path_elements) - i) \
1418                          + map(lambda n: n.name, other.path_elements[i:])
1419
1420             result = string.join(path_elems, os.sep)
1421
1422         memo_dict[other] = result
1423
1424         return result
1425
1426     def get_env_scanner(self, env, kw={}):
1427         import SCons.Defaults
1428         return SCons.Defaults.DirEntryScanner
1429
1430     def get_target_scanner(self):
1431         import SCons.Defaults
1432         return SCons.Defaults.DirEntryScanner
1433
1434     def get_found_includes(self, env, scanner, path):
1435         """Return this directory's implicit dependencies.
1436
1437         We don't bother caching the results because the scan typically
1438         shouldn't be requested more than once (as opposed to scanning
1439         .h file contents, which can be requested as many times as the
1440         files is #included by other files).
1441         """
1442         if not scanner:
1443             return []
1444         # Clear cached info for this Dir.  If we already visited this
1445         # directory on our walk down the tree (because we didn't know at
1446         # that point it was being used as the source for another Node)
1447         # then we may have calculated build signature before realizing
1448         # we had to scan the disk.  Now that we have to, though, we need
1449         # to invalidate the old calculated signature so that any node
1450         # dependent on our directory structure gets one that includes
1451         # info about everything on disk.
1452         self.clear()
1453         return scanner(self, env, path)
1454
1455     def build(self, **kw):
1456         """A null "builder" for directories."""
1457         global MkdirBuilder
1458         if not self.builder is MkdirBuilder:
1459             apply(SCons.Node.Node.build, [self,], kw)
1460
1461     def _create(self):
1462         """Create this directory, silently and without worrying about
1463         whether the builder is the default or not."""
1464         listDirs = []
1465         parent = self
1466         while parent:
1467             if parent.exists():
1468                 break
1469             listDirs.append(parent)
1470             p = parent.up()
1471             if p is None:
1472                 raise SCons.Errors.StopError, parent.path
1473             parent = p
1474         listDirs.reverse()
1475         for dirnode in listDirs:
1476             try:
1477                 # Don't call dirnode.build(), call the base Node method
1478                 # directly because we definitely *must* create this
1479                 # directory.  The dirnode.build() method will suppress
1480                 # the build if it's the default builder.
1481                 SCons.Node.Node.build(dirnode)
1482                 dirnode.get_executor().nullify()
1483                 # The build() action may or may not have actually
1484                 # created the directory, depending on whether the -n
1485                 # option was used or not.  Delete the _exists and
1486                 # _rexists attributes so they can be reevaluated.
1487                 dirnode.clear()
1488             except OSError:
1489                 pass
1490
1491     def multiple_side_effect_has_builder(self):
1492         global MkdirBuilder
1493         return not self.builder is MkdirBuilder and self.has_builder()
1494
1495     def alter_targets(self):
1496         """Return any corresponding targets in a build directory.
1497         """
1498         return self.fs.build_dir_target_climb(self, self, [])
1499
1500     def scanner_key(self):
1501         """A directory does not get scanned."""
1502         return None
1503
1504     def get_contents(self):
1505         """Return aggregate contents of all our children."""
1506         contents = map(lambda n: n.get_contents(), self.children())
1507         return  string.join(contents, '')
1508
1509     def prepare(self):
1510         pass
1511
1512     def do_duplicate(self, src):
1513         pass
1514
1515     def current(self, calc=None):
1516         """If any child is not up-to-date, then this directory isn't,
1517         either."""
1518         if not self.builder is MkdirBuilder and not self.exists():
1519             return 0
1520         up_to_date = SCons.Node.up_to_date
1521         for kid in self.children():
1522             if kid.get_state() > up_to_date:
1523                 return 0
1524         return 1
1525
1526     def rdir(self):
1527         if not self.exists():
1528             norm_name = _my_normcase(self.name)
1529             for dir in self.dir.get_all_rdirs():
1530                 try: node = dir.entries[norm_name]
1531                 except KeyError: node = dir.dir_on_disk(self.name)
1532                 if node and node.exists() and \
1533                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1534                         return node
1535         return self
1536
1537     def sconsign(self):
1538         """Return the .sconsign file info for this directory,
1539         creating it first if necessary."""
1540         if not self._sconsign:
1541             import SCons.SConsign
1542             self._sconsign = SCons.SConsign.ForDirectory(self)
1543         return self._sconsign
1544
1545     def srcnode(self):
1546         """Dir has a special need for srcnode()...if we
1547         have a srcdir attribute set, then that *is* our srcnode."""
1548         if self.srcdir:
1549             return self.srcdir
1550         return Base.srcnode(self)
1551
1552     def get_timestamp(self):
1553         """Return the latest timestamp from among our children"""
1554         stamp = 0
1555         for kid in self.children():
1556             if kid.get_timestamp() > stamp:
1557                 stamp = kid.get_timestamp()
1558         return stamp
1559
1560     def entry_abspath(self, name):
1561         return self.abspath + os.sep + name
1562
1563     def entry_path(self, name):
1564         return self.path + os.sep + name
1565
1566     def entry_tpath(self, name):
1567         return self.tpath + os.sep + name
1568
1569     def entry_exists_on_disk(self, name):
1570         try:
1571             d = self.on_disk_entries
1572         except AttributeError:
1573             d = {}
1574             try:
1575                 entries = os.listdir(self.abspath)
1576             except OSError:
1577                 pass
1578             else:
1579                 for entry in map(_my_normcase, entries):
1580                     d[entry] = 1
1581             self.on_disk_entries = d
1582         return d.has_key(_my_normcase(name))
1583
1584     memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
1585
1586     def srcdir_list(self):
1587         try:
1588             return self._memo['srcdir_list']
1589         except KeyError:
1590             pass
1591
1592         result = []
1593
1594         dirname = '.'
1595         dir = self
1596         while dir:
1597             if dir.srcdir:
1598                 d = dir.srcdir.Dir(dirname)
1599                 if d.is_under(dir):
1600                     # Shouldn't source from something in the build path:
1601                     # build_dir is probably under src_dir, in which case
1602                     # we are reflecting.
1603                     break
1604                 result.append(d)
1605             dirname = dir.name + os.sep + dirname
1606             dir = dir.up()
1607
1608         self._memo['srcdir_list'] = result
1609
1610         return result
1611
1612     def srcdir_duplicate(self, name):
1613         for dir in self.srcdir_list():
1614             if dir.entry_exists_on_disk(name):
1615                 srcnode = dir.Entry(name).disambiguate()
1616                 if self.duplicate:
1617                     node = self.Entry(name).disambiguate()
1618                     node.do_duplicate(srcnode)
1619                     return node
1620                 else:
1621                     return srcnode
1622         return None
1623
1624     def _srcdir_find_file_key(self, filename):
1625         return filename
1626
1627     memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
1628
1629     def srcdir_find_file(self, filename):
1630         try:
1631             memo_dict = self._memo['srcdir_find_file']
1632         except KeyError:
1633             memo_dict = {}
1634             self._memo['srcdir_find_file'] = memo_dict
1635         else:
1636             try:
1637                 return memo_dict[filename]
1638             except KeyError:
1639                 pass
1640
1641         def func(node):
1642             if (isinstance(node, File) or isinstance(node, Entry)) and \
1643                (node.is_derived() or node.is_pseudo_derived() or node.exists()):
1644                     return node
1645             return None
1646
1647         norm_name = _my_normcase(filename)
1648
1649         for rdir in self.get_all_rdirs():
1650             try: node = rdir.entries[norm_name]
1651             except KeyError: node = rdir.file_on_disk(filename)
1652             else: node = func(node)
1653             if node:
1654                 result = (node, self)
1655                 memo_dict[filename] = result
1656                 return result
1657
1658         for srcdir in self.srcdir_list():
1659             for rdir in srcdir.get_all_rdirs():
1660                 try: node = rdir.entries[norm_name]
1661                 except KeyError: node = rdir.file_on_disk(filename)
1662                 else: node = func(node)
1663                 if node:
1664                     result = (File(filename, self, self.fs), srcdir)
1665                     memo_dict[filename] = result
1666                     return result
1667
1668         result = (None, None)
1669         memo_dict[filename] = result
1670         return result
1671
1672     def dir_on_disk(self, name):
1673         if self.entry_exists_on_disk(name):
1674             try: return self.Dir(name)
1675             except TypeError: pass
1676         return None
1677
1678     def file_on_disk(self, name):
1679         if self.entry_exists_on_disk(name) or \
1680            diskcheck_rcs(self, name) or \
1681            diskcheck_sccs(self, name):
1682             try: return self.File(name)
1683             except TypeError: pass
1684         node = self.srcdir_duplicate(name)
1685         if isinstance(node, Dir):
1686             node = None
1687         return node
1688
1689     def walk(self, func, arg):
1690         """
1691         Walk this directory tree by calling the specified function
1692         for each directory in the tree.
1693
1694         This behaves like the os.path.walk() function, but for in-memory
1695         Node.FS.Dir objects.  The function takes the same arguments as
1696         the functions passed to os.path.walk():
1697
1698                 func(arg, dirname, fnames)
1699
1700         Except that "dirname" will actually be the directory *Node*,
1701         not the string.  The '.' and '..' entries are excluded from
1702         fnames.  The fnames list may be modified in-place to filter the
1703         subdirectories visited or otherwise impose a specific order.
1704         The "arg" argument is always passed to func() and may be used
1705         in any way (or ignored, passing None is common).
1706         """
1707         entries = self.entries
1708         names = entries.keys()
1709         names.remove('.')
1710         names.remove('..')
1711         func(arg, self, names)
1712         select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
1713         for dirname in filter(select_dirs, names):
1714             entries[dirname].walk(func, arg)
1715
1716 class RootDir(Dir):
1717     """A class for the root directory of a file system.
1718
1719     This is the same as a Dir class, except that the path separator
1720     ('/' or '\\') is actually part of the name, so we don't need to
1721     add a separator when creating the path names of entries within
1722     this directory.
1723     """
1724     def __init__(self, name, fs):
1725         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1726         # We're going to be our own parent directory (".." entry and .dir
1727         # attribute) so we have to set up some values so Base.__init__()
1728         # won't gag won't it calls some of our methods.
1729         self.abspath = ''
1730         self.path = ''
1731         self.tpath = ''
1732         self.path_elements = []
1733         self.duplicate = 0
1734         Base.__init__(self, name, self, fs)
1735
1736         # Now set our paths to what we really want them to be: the
1737         # initial drive letter (the name) plus the directory separator.
1738         self.abspath = name + os.sep
1739         self.path = name + os.sep
1740         self.tpath = name + os.sep
1741         self._morph()
1742
1743     def must_be_same(self, klass):
1744         if klass is Dir:
1745             return
1746         Base.must_be_same(self, klass)
1747
1748     def __str__(self):
1749         return self.abspath
1750
1751     def entry_abspath(self, name):
1752         return self.abspath + name
1753
1754     def entry_path(self, name):
1755         return self.path + name
1756
1757     def entry_tpath(self, name):
1758         return self.tpath + name
1759
1760     def is_under(self, dir):
1761         if self is dir:
1762             return 1
1763         else:
1764             return 0
1765
1766     def up(self):
1767         return None
1768
1769     def get_dir(self):
1770         return None
1771
1772     def src_builder(self):
1773         return _null
1774
1775 class FileNodeInfo(SCons.Node.NodeInfoBase):
1776     def __init__(self, node):
1777         SCons.Node.NodeInfoBase.__init__(self, node)
1778         self.update(node)
1779     def __cmp__(self, other):
1780         try: return cmp(self.bsig, other.bsig)
1781         except AttributeError: return 1
1782     def update(self, node):
1783         self.timestamp = node.get_timestamp()
1784         self.size = node.getsize()
1785
1786 class FileBuildInfo(SCons.Node.BuildInfoBase):
1787     def __init__(self, node):
1788         SCons.Node.BuildInfoBase.__init__(self, node)
1789         self.node = node
1790     def convert_to_sconsign(self):
1791         """Convert this FileBuildInfo object for writing to a .sconsign file
1792
1793         We hung onto the node that we refer to so that we can translate
1794         the lists of bsources, bdepends and bimplicit Nodes into strings
1795         relative to the node, but we don't want to write out that Node
1796         itself to the .sconsign file, so we delete the attribute in
1797         preparation.
1798         """
1799         rel_path = self.node.rel_path
1800         delattr(self, 'node')
1801         for attr in ['bsources', 'bdepends', 'bimplicit']:
1802             try:
1803                 val = getattr(self, attr)
1804             except AttributeError:
1805                 pass
1806             else:
1807                 setattr(self, attr, map(rel_path, val))
1808     def convert_from_sconsign(self, dir, name):
1809         """Convert a newly-read FileBuildInfo object for in-SCons use
1810
1811         An on-disk BuildInfo comes without a reference to the node for
1812         which it's intended, so we have to convert the arguments and add
1813         back a self.node attribute.  We don't worry here about converting
1814         the bsources, bdepends and bimplicit lists from strings to Nodes
1815         because they're not used in the normal case of just deciding
1816         whether or not to rebuild things.
1817         """
1818         self.node = dir.Entry(name)
1819     def prepare_dependencies(self):
1820         """Prepare a FileBuildInfo object for explaining what changed
1821
1822         The bsources, bdepends and bimplicit lists have all been stored
1823         on disk as paths relative to the Node for which they're stored
1824         as dependency info.  Convert the strings to actual Nodes (for
1825         use by the --debug=explain code and --implicit-cache).
1826         """
1827         def str_to_node(s, entry=self.node.dir.Entry):
1828             # This is a little bogus; we're going to mimic the lookup
1829             # order of env.arg2nodes() by hard-coding an Alias lookup
1830             # before we assume it's an Entry.  This should be able to
1831             # go away once the Big Signature Refactoring pickles the
1832             # actual NodeInfo object, which will let us know precisely
1833             # what type of Node to turn it into.
1834             import SCons.Node.Alias
1835             n = SCons.Node.Alias.default_ans.lookup(s)
1836             if not n:
1837                 n = entry(s)
1838             return n
1839         for attr in ['bsources', 'bdepends', 'bimplicit']:
1840             try:
1841                 val = getattr(self, attr)
1842             except AttributeError:
1843                 pass
1844             else:
1845                 setattr(self, attr, map(str_to_node, val))
1846     def format(self):
1847         result = [ self.ninfo.format() ]
1848         bkids = self.bsources + self.bdepends + self.bimplicit
1849         bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
1850         for i in xrange(len(bkids)):
1851             result.append(str(bkids[i]) + ': ' + bkidsigs[i].format())
1852         return string.join(result, '\n')
1853
1854 class NodeInfo(FileNodeInfo):
1855     pass
1856
1857 class BuildInfo(FileBuildInfo):
1858     pass
1859
1860 class File(Base):
1861     """A class for files in a file system.
1862     """
1863
1864     memoizer_counters = []
1865
1866     NodeInfo = FileNodeInfo
1867     BuildInfo = FileBuildInfo
1868
1869     def diskcheck_match(self):
1870         diskcheck_match(self, self.isdir,
1871                         "Directory %s found where file expected.")
1872
1873     def __init__(self, name, directory, fs):
1874         if __debug__: logInstanceCreation(self, 'Node.FS.File')
1875         Base.__init__(self, name, directory, fs)
1876         self._morph()
1877
1878     def Entry(self, name):
1879         """Create an entry node named 'name' relative to
1880         the SConscript directory of this file."""
1881         return self.cwd.Entry(name)
1882
1883     def Dir(self, name):
1884         """Create a directory node named 'name' relative to
1885         the SConscript directory of this file."""
1886         return self.cwd.Dir(name)
1887
1888     def Dirs(self, pathlist):
1889         """Create a list of directories relative to the SConscript
1890         directory of this file."""
1891         return map(lambda p, s=self: s.Dir(p), pathlist)
1892
1893     def File(self, name):
1894         """Create a file node named 'name' relative to
1895         the SConscript directory of this file."""
1896         return self.cwd.File(name)
1897
1898     #def generate_build_dict(self):
1899     #    """Return an appropriate dictionary of values for building
1900     #    this File."""
1901     #    return {'Dir' : self.Dir,
1902     #            'File' : self.File,
1903     #            'RDirs' : self.RDirs}
1904
1905     def _morph(self):
1906         """Turn a file system node into a File object."""
1907         self.scanner_paths = {}
1908         if not hasattr(self, '_local'):
1909             self._local = 0
1910
1911     def scanner_key(self):
1912         return self.get_suffix()
1913
1914     def get_contents(self):
1915         if not self.rexists():
1916             return ''
1917         fname = self.rfile().abspath
1918         try:
1919             r = open(fname, "rb").read()
1920         except EnvironmentError, e:
1921             if not e.filename:
1922                 e.filename = fname
1923             raise
1924         return r
1925
1926     def get_timestamp(self):
1927         if self.rexists():
1928             return self.rfile().getmtime()
1929         else:
1930             return 0
1931
1932     def store_info(self, obj):
1933         # Merge our build information into the already-stored entry.
1934         # This accomodates "chained builds" where a file that's a target
1935         # in one build (SConstruct file) is a source in a different build.
1936         # See test/chained-build.py for the use case.
1937         entry = self.get_stored_info()
1938         entry.merge(obj)
1939         self.dir.sconsign().set_entry(self.name, entry)
1940
1941     def get_stored_info(self):
1942         try:
1943             stored = self.dir.sconsign().get_entry(self.name)
1944         except (KeyError, OSError):
1945             return self.new_binfo()
1946         else:
1947             if not hasattr(stored, 'ninfo'):
1948                 # Transition:  The .sconsign file entry has no NodeInfo
1949                 # object, which means it's a slightly older BuildInfo.
1950                 # Copy over the relevant attributes.
1951                 ninfo = stored.ninfo = self.new_ninfo()
1952                 for attr in ninfo.__dict__.keys():
1953                     try:
1954                         setattr(ninfo, attr, getattr(stored, attr))
1955                     except AttributeError:
1956                         pass
1957             return stored
1958
1959     def get_stored_implicit(self):
1960         binfo = self.get_stored_info()
1961         binfo.prepare_dependencies()
1962         try: return binfo.bimplicit
1963         except AttributeError: return None
1964
1965     def rel_path(self, other):
1966         return self.dir.rel_path(other)
1967
1968     def _get_found_includes_key(self, env, scanner, path):
1969         return (id(env), id(scanner), path)
1970
1971     memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
1972
1973     def get_found_includes(self, env, scanner, path):
1974         """Return the included implicit dependencies in this file.
1975         Cache results so we only scan the file once per path
1976         regardless of how many times this information is requested.
1977         """
1978         memo_key = (id(env), id(scanner), path)
1979         try:
1980             memo_dict = self._memo['get_found_includes']
1981         except KeyError:
1982             memo_dict = {}
1983             self._memo['get_found_includes'] = memo_dict
1984         else:
1985             try:
1986                 return memo_dict[memo_key]
1987             except KeyError:
1988                 pass
1989
1990         if scanner:
1991             result = scanner(self, env, path)
1992             result = map(lambda N: N.disambiguate(), result)
1993         else:
1994             result = []
1995
1996         memo_dict[memo_key] = result
1997
1998         return result
1999
2000     def _createDir(self):
2001         # ensure that the directories for this node are
2002         # created.
2003         self.dir._create()
2004
2005     def retrieve_from_cache(self):
2006         """Try to retrieve the node's content from a cache
2007
2008         This method is called from multiple threads in a parallel build,
2009         so only do thread safe stuff here. Do thread unsafe stuff in
2010         built().
2011
2012         Returns true iff the node was successfully retrieved.
2013         """
2014         if self.nocache:
2015             return None
2016         if not self.is_derived():
2017             return None
2018         return self.get_build_env().get_CacheDir().retrieve(self)
2019
2020     def built(self):
2021         """
2022         Called just after this node is successfully built.
2023         """
2024         # Push this file out to cache before the superclass Node.built()
2025         # method has a chance to clear the build signature, which it
2026         # will do if this file has a source scanner.
2027         #
2028         # We have to clear the memoized values *before* we push it to
2029         # cache so that the memoization of the self.exists() return
2030         # value doesn't interfere.
2031         self.clear_memoized_values()
2032         if self.exists():
2033             self.get_build_env().get_CacheDir().push(self)
2034         SCons.Node.Node.built(self)
2035
2036     def visited(self):
2037         if self.exists():
2038             self.get_build_env().get_CacheDir().push_if_forced(self)
2039
2040     def has_src_builder(self):
2041         """Return whether this Node has a source builder or not.
2042
2043         If this Node doesn't have an explicit source code builder, this
2044         is where we figure out, on the fly, if there's a transparent
2045         source code builder for it.
2046
2047         Note that if we found a source builder, we also set the
2048         self.builder attribute, so that all of the methods that actually
2049         *build* this file don't have to do anything different.
2050         """
2051         try:
2052             scb = self.sbuilder
2053         except AttributeError:
2054             if self.rexists():
2055                 scb = None
2056             else:
2057                 scb = self.dir.src_builder()
2058                 if scb is _null:
2059                     if diskcheck_sccs(self.dir, self.name):
2060                         scb = get_DefaultSCCSBuilder()
2061                     elif diskcheck_rcs(self.dir, self.name):
2062                         scb = get_DefaultRCSBuilder()
2063                     else:
2064                         scb = None
2065                 if scb is not None:
2066                     self.builder_set(scb)
2067             self.sbuilder = scb
2068         return not scb is None
2069
2070     def alter_targets(self):
2071         """Return any corresponding targets in a build directory.
2072         """
2073         if self.is_derived():
2074             return [], None
2075         return self.fs.build_dir_target_climb(self, self.dir, [self.name])
2076
2077     def is_pseudo_derived(self):
2078         return self.has_src_builder()
2079
2080     def _rmv_existing(self):
2081         self.clear_memoized_values()
2082         Unlink(self, [], None)
2083
2084     def prepare(self):
2085         """Prepare for this file to be created."""
2086         SCons.Node.Node.prepare(self)
2087
2088         if self.get_state() != SCons.Node.up_to_date:
2089             if self.exists():
2090                 if self.is_derived() and not self.precious:
2091                     self._rmv_existing()
2092             else:
2093                 try:
2094                     self._createDir()
2095                 except SCons.Errors.StopError, drive:
2096                     desc = "No drive `%s' for target `%s'." % (drive, self)
2097                     raise SCons.Errors.StopError, desc
2098
2099     def remove(self):
2100         """Remove this file."""
2101         if self.exists() or self.islink():
2102             self.fs.unlink(self.path)
2103             return 1
2104         return None
2105
2106     def do_duplicate(self, src):
2107         self._createDir()
2108         try:
2109             Unlink(self, None, None)
2110         except SCons.Errors.BuildError:
2111             pass
2112         try:
2113             Link(self, src, None)
2114         except SCons.Errors.BuildError, e:
2115             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
2116             raise SCons.Errors.StopError, desc
2117         self.linked = 1
2118         # The Link() action may or may not have actually
2119         # created the file, depending on whether the -n
2120         # option was used or not.  Delete the _exists and
2121         # _rexists attributes so they can be reevaluated.
2122         self.clear()
2123
2124     memoizer_counters.append(SCons.Memoize.CountValue('exists'))
2125
2126     def exists(self):
2127         try:
2128             return self._memo['exists']
2129         except KeyError:
2130             pass
2131         # Duplicate from source path if we are set up to do this.
2132         if self.duplicate and not self.is_derived() and not self.linked:
2133             src = self.srcnode()
2134             if not src is self:
2135                 # At this point, src is meant to be copied in a build directory.
2136                 src = src.rfile()
2137                 if src.abspath != self.abspath:
2138                     if src.exists():
2139                         self.do_duplicate(src)
2140                         # Can't return 1 here because the duplication might
2141                         # not actually occur if the -n option is being used.
2142                     else:
2143                         # The source file does not exist.  Make sure no old
2144                         # copy remains in the build directory.
2145                         if Base.exists(self) or self.islink():
2146                             self.fs.unlink(self.path)
2147                         # Return None explicitly because the Base.exists() call
2148                         # above will have cached its value if the file existed.
2149                         self._memo['exists'] = None
2150                         return None
2151         result = Base.exists(self)
2152         self._memo['exists'] = result
2153         return result
2154
2155     #
2156     # SIGNATURE SUBSYSTEM
2157     #
2158
2159     def get_csig(self, calc=None):
2160         """
2161         Generate a node's content signature, the digested signature
2162         of its content.
2163
2164         node - the node
2165         cache - alternate node to use for the signature cache
2166         returns - the content signature
2167         """
2168         try:
2169             return self.binfo.ninfo.csig
2170         except AttributeError:
2171             pass
2172
2173         if calc is None:
2174             calc = self.calculator()
2175
2176         max_drift = self.fs.max_drift
2177         mtime = self.get_timestamp()
2178         use_stored = max_drift >= 0 and (time.time() - mtime) > max_drift
2179
2180         csig = None
2181         if use_stored:
2182             old = self.get_stored_info().ninfo
2183             try:
2184                 if old.timestamp and old.csig and old.timestamp == mtime:
2185                     csig = old.csig
2186             except AttributeError:
2187                 pass
2188         if csig is None:
2189             csig = calc.module.signature(self)
2190
2191         binfo = self.get_binfo()
2192         ninfo = binfo.ninfo
2193         ninfo.csig = csig
2194         ninfo.update(self)
2195
2196         if use_stored:
2197             self.store_info(binfo)
2198
2199         return csig
2200
2201     #
2202     #
2203     #
2204
2205     def is_up_to_date(self, node=None, bi=None):
2206         """Returns if the node is up-to-date with respect to stored
2207         BuildInfo.  The default is to compare it against our own
2208         previously stored BuildInfo, but the stored BuildInfo from another
2209         Node (typically one in a Repository) can be used instead."""
2210         if bi is None:
2211             if node is None:
2212                 node = self
2213             bi = node.get_stored_info()
2214         new = self.get_binfo()
2215         return new == bi
2216
2217     def current(self, calc=None):
2218         self.binfo = self.gen_binfo(calc)
2219         return self._cur2()
2220     def _cur2(self):
2221         if not self.exists():
2222             # The file doesn't exist locally...
2223             r = self.rfile()
2224             if r != self:
2225                 # ...but there is one in a Repository...
2226                 if self.is_up_to_date(r):
2227                     # ...and it's even up-to-date...
2228                     if self._local:
2229                         # ...and they'd like a local copy.
2230                         LocalCopy(self, r, None)
2231                         self.store_info(self.get_binfo())
2232                     return 1
2233             return None
2234         else:
2235             return self.is_up_to_date()
2236
2237     memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
2238
2239     def rfile(self):
2240         try:
2241             return self._memo['rfile']
2242         except KeyError:
2243             pass
2244         result = self
2245         if not self.exists():
2246             norm_name = _my_normcase(self.name)
2247             for dir in self.dir.get_all_rdirs():
2248                 try: node = dir.entries[norm_name]
2249                 except KeyError: node = dir.file_on_disk(self.name)
2250                 if node and node.exists() and \
2251                    (isinstance(node, File) or isinstance(node, Entry) \
2252                     or not node.is_derived()):
2253                         result = node
2254                         break
2255         self._memo['rfile'] = result
2256         return result
2257
2258     def rstr(self):
2259         return str(self.rfile())
2260
2261     def get_cachedir_bsig(self):
2262         import SCons.Sig.MD5
2263         ninfo = self.get_binfo().ninfo
2264         if not hasattr(ninfo, 'bsig'):
2265             import SCons.Errors
2266             raise SCons.Errors.InternalError, "cachepath(%s) found no bsig" % self.path
2267         elif ninfo.bsig is None:
2268             import SCons.Errors
2269             raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
2270         # Add the path to the cache signature, because multiple
2271         # targets built by the same action will all have the same
2272         # build signature, and we have to differentiate them somehow.
2273         return SCons.Sig.MD5.collect([ninfo.bsig, self.path])
2274
2275 default_fs = None
2276
2277 def get_default_fs():
2278     global default_fs
2279     if not default_fs:
2280         default_fs = FS()
2281     return default_fs
2282
2283 class FileFinder:
2284     """
2285     """
2286     if SCons.Memoize.use_memoizer:
2287         __metaclass__ = SCons.Memoize.Memoized_Metaclass
2288
2289     memoizer_counters = []
2290
2291     def __init__(self):
2292         self._memo = {}
2293
2294     def _find_file_key(self, filename, paths, verbose=None):
2295         return (filename, paths)
2296         
2297     memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
2298
2299     def find_file(self, filename, paths, verbose=None):
2300         """
2301         find_file(str, [Dir()]) -> [nodes]
2302
2303         filename - a filename to find
2304         paths - a list of directory path *nodes* to search in.  Can be
2305                 represented as a list, a tuple, or a callable that is
2306                 called with no arguments and returns the list or tuple.
2307
2308         returns - the node created from the found file.
2309
2310         Find a node corresponding to either a derived file or a file
2311         that exists already.
2312
2313         Only the first file found is returned, and none is returned
2314         if no file is found.
2315         """
2316         memo_key = self._find_file_key(filename, paths)
2317         try:
2318             memo_dict = self._memo['find_file']
2319         except KeyError:
2320             memo_dict = {}
2321             self._memo['find_file'] = memo_dict
2322         else:
2323             try:
2324                 return memo_dict[memo_key]
2325             except KeyError:
2326                 pass
2327
2328         if verbose:
2329             if not SCons.Util.is_String(verbose):
2330                 verbose = "find_file"
2331             if not callable(verbose):
2332                 verbose = '  %s: ' % verbose
2333                 verbose = lambda s, v=verbose: sys.stdout.write(v + s)
2334         else:
2335             verbose = lambda x: x
2336
2337         filedir, filename = os.path.split(filename)
2338         if filedir:
2339             def filedir_lookup(p, fd=filedir):
2340                 try:
2341                     return p.Dir(fd)
2342                 except TypeError:
2343                     # We tried to look up a Dir, but it seems there's
2344                     # already a File (or something else) there.  No big.
2345                     return None
2346             paths = filter(None, map(filedir_lookup, paths))
2347
2348         result = None
2349         for dir in paths:
2350             verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
2351             node, d = dir.srcdir_find_file(filename)
2352             if node:
2353                 verbose("... FOUND '%s' in '%s'\n" % (filename, d))
2354                 result = node
2355                 break
2356
2357         memo_dict[memo_key] = result
2358
2359         return result
2360
2361 find_file = FileFinder().find_file