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