Eliminate Executor's creation and use of a build_dict and a subst_dict, which were...
[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 initializes a "default_fs" Node with an FS at the current directory
9 for its own purposes, and for use by scripts or modules looking for the
10 canonical default.
11
12 """
13
14 #
15 # __COPYRIGHT__
16 #
17 # Permission is hereby granted, free of charge, to any person obtaining
18 # a copy of this software and associated documentation files (the
19 # "Software"), to deal in the Software without restriction, including
20 # without limitation the rights to use, copy, modify, merge, publish,
21 # distribute, sublicense, and/or sell copies of the Software, and to
22 # permit persons to whom the Software is furnished to do so, subject to
23 # the following conditions:
24 #
25 # The above copyright notice and this permission notice shall be included
26 # in all copies or substantial portions of the Software.
27 #
28 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
29 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
30 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
32 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
33 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 #
36
37 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
38
39 import os
40 import os.path
41 import shutil
42 import stat
43 import string
44 import sys
45 import time
46 import cStringIO
47
48 import SCons.Action
49 from SCons.Debug import logInstanceCreation
50 import SCons.Errors
51 import SCons.Node
52 import SCons.Sig.MD5
53 import SCons.Util
54 import SCons.Warnings
55
56 #
57 # We stringify these file system Nodes a lot.  Turning a file system Node
58 # into a string is non-trivial, because the final string representation
59 # can depend on a lot of factors:  whether it's a derived target or not,
60 # whether it's linked to a repository or source directory, and whether
61 # there's duplication going on.  The normal technique for optimizing
62 # calculations like this is to memoize (cache) the string value, so you
63 # only have to do the calculation once.
64 #
65 # A number of the above factors, however, can be set after we've already
66 # been asked to return a string for a Node, because a Repository() or
67 # BuildDir() call or the like may not occur until later in SConscript
68 # files.  So this variable controls whether we bother trying to save
69 # string values for Nodes.  The wrapper interface can set this whenever
70 # they're done mucking with Repository and BuildDir and the other stuff,
71 # to let this module know it can start returning saved string values
72 # for Nodes.
73 #
74 Save_Strings = None
75
76 def save_strings(val):
77     global Save_Strings
78     Save_Strings = val
79
80 #
81 # SCons.Action objects for interacting with the outside world.
82 #
83 # The Node.FS methods in this module should use these actions to
84 # create and/or remove files and directories; they should *not* use
85 # os.{link,symlink,unlink,mkdir}(), etc., directly.
86 #
87 # Using these SCons.Action objects ensures that descriptions of these
88 # external activities are properly displayed, that the displays are
89 # suppressed when the -s (silent) option is used, and (most importantly)
90 # the actions are disabled when the the -n option is used, in which case
91 # there should be *no* changes to the external file system(s)...
92 #
93
94 def _copy_func(src, dest):
95     shutil.copy2(src, dest)
96     st=os.stat(src)
97     os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
98
99 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
100                     'hard-copy', 'soft-copy', 'copy']
101
102 Link_Funcs = [] # contains the callables of the specified duplication style
103
104 def set_duplicate(duplicate):
105     # Fill in the Link_Funcs list according to the argument
106     # (discarding those not available on the platform).
107
108     # Set up the dictionary that maps the argument names to the
109     # underlying implementations.  We do this inside this function,
110     # not in the top-level module code, so that we can remap os.link
111     # and os.symlink for testing purposes.
112     try:
113         _hardlink_func = os.link
114     except AttributeError:
115         _hardlink_func = None
116
117     try:
118         _softlink_func = os.symlink
119     except AttributeError:
120         _softlink_func = None
121
122     link_dict = {
123         'hard' : _hardlink_func,
124         'soft' : _softlink_func,
125         'copy' : _copy_func
126     }
127
128     if not duplicate in Valid_Duplicates:
129         raise SCons.Errors.InternalError, ("The argument of set_duplicate "
130                                            "should be in Valid_Duplicates")
131     global Link_Funcs
132     Link_Funcs = []
133     for func in string.split(duplicate,'-'):
134         if link_dict[func]:
135             Link_Funcs.append(link_dict[func])
136
137 def LinkFunc(target, source, env):
138     # Relative paths cause problems with symbolic links, so
139     # we use absolute paths, which may be a problem for people
140     # who want to move their soft-linked src-trees around. Those
141     # people should use the 'hard-copy' mode, softlinks cannot be
142     # used for that; at least I have no idea how ...
143     src = source[0].abspath
144     dest = target[0].abspath
145     dir, file = os.path.split(dest)
146     if dir and not os.path.isdir(dir):
147         os.makedirs(dir)
148     if not Link_Funcs:
149         # Set a default order of link functions.
150         set_duplicate('hard-soft-copy')
151     # Now link the files with the previously specified order.
152     for func in Link_Funcs:
153         try:
154             func(src,dest)
155             break
156         except OSError:
157             if func == Link_Funcs[-1]:
158                 # exception of the last link method (copy) are fatal
159                 raise
160             else:
161                 pass
162     return 0
163
164 Link = SCons.Action.Action(LinkFunc, None)
165 def LocalString(target, source, env):
166     return 'Local copy of %s from %s' % (target[0], source[0])
167
168 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
169
170 def UnlinkFunc(target, source, env):
171     t = target[0]
172     t.fs.unlink(t.abspath)
173     return 0
174
175 Unlink = SCons.Action.Action(UnlinkFunc, None)
176
177 def MkdirFunc(target, source, env):
178     t = target[0]
179     p = t.abspath
180     if not t.fs.exists(p):
181         t.fs.mkdir(p)
182     return 0
183
184 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
185
186 MkdirBuilder = None
187
188 def get_MkdirBuilder():
189     global MkdirBuilder
190     if MkdirBuilder is None:
191         import SCons.Builder
192         # "env" will get filled in by Executor.get_build_env()
193         # calling SCons.Defaults.DefaultEnvironment() when necessary.
194         MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
195                                              env = None,
196                                              explain = None,
197                                              is_explicit = None,
198                                              name = "MkdirBuilder")
199     return MkdirBuilder
200
201 def CacheRetrieveFunc(target, source, env):
202     t = target[0]
203     fs = t.fs
204     cachedir, cachefile = t.cachepath()
205     if fs.exists(cachefile):
206         if SCons.Action.execute_actions:
207             fs.copy2(cachefile, t.path)
208             st = fs.stat(cachefile)
209             fs.chmod(t.path, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
210         return 0
211     return 1
212
213 def CacheRetrieveString(target, source, env):
214     t = target[0]
215     cachedir, cachefile = t.cachepath()
216     if t.fs.exists(cachefile):
217         return "Retrieved `%s' from cache" % t.path
218     return None
219
220 CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)
221
222 CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
223
224 def CachePushFunc(target, source, env):
225     t = target[0]
226     fs = t.fs
227     cachedir, cachefile = t.cachepath()
228     if fs.exists(cachefile):
229         # Don't bother copying it if it's already there.
230         return
231
232     if not fs.isdir(cachedir):
233         fs.makedirs(cachedir)
234
235     tempfile = cachefile+'.tmp'
236     try:
237         fs.copy2(t.path, tempfile)
238         fs.rename(tempfile, cachefile)
239         st = fs.stat(t.path)
240         fs.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
241     except OSError:
242         # It's possible someone else tried writing the file at the same
243         # time we did.  Print a warning but don't stop the build, since
244         # it doesn't affect the correctness of the build.
245         SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning,
246                             "Unable to copy %s to cache. Cache file is %s"
247                                 % (str(target), cachefile))
248         return
249
250 CachePush = SCons.Action.Action(CachePushFunc, None)
251
252 class _Null:
253     pass
254
255 _null = _Null()
256
257 DefaultSCCSBuilder = None
258 DefaultRCSBuilder = None
259
260 def get_DefaultSCCSBuilder():
261     global DefaultSCCSBuilder
262     if DefaultSCCSBuilder is None:
263         import SCons.Builder
264         # "env" will get filled in by Executor.get_build_env()
265         # calling SCons.Defaults.DefaultEnvironment() when necessary.
266         act = SCons.Action.Action('$SCCSCOM', '$SCCSCOMSTR')
267         DefaultSCCSBuilder = SCons.Builder.Builder(action = act,
268                                                    env = None,
269                                                    name = "DefaultSCCSBuilder")
270     return DefaultSCCSBuilder
271
272 def get_DefaultRCSBuilder():
273     global DefaultRCSBuilder
274     if DefaultRCSBuilder is None:
275         import SCons.Builder
276         # "env" will get filled in by Executor.get_build_env()
277         # calling SCons.Defaults.DefaultEnvironment() when necessary.
278         act = SCons.Action.Action('$RCS_COCOM', '$RCS_COCOMSTR')
279         DefaultRCSBuilder = SCons.Builder.Builder(action = act,
280                                                   env = None,
281                                                   name = "DefaultRCSBuilder")
282     return DefaultRCSBuilder
283
284 #
285 class ParentOfRoot:
286     """
287     An instance of this class is used as the parent of the root of a
288     filesystem (POSIX) or drive (Win32). This isn't actually a node,
289     but it looks enough like one so that we don't have to have
290     special purpose code everywhere to deal with dir being None. 
291     This class is an instance of the Null object pattern.
292     """
293     def __init__(self):
294         self.abspath = ''
295         self.path = ''
296         self.name=''
297         self.duplicate=0
298         self.srcdir=None
299         self.build_dirs=[]
300         
301     def is_under(self, dir):
302         return 0
303
304     def up(self):
305         return None
306
307     def getRepositories(self):
308         return []
309
310     def get_dir(self):
311         return None
312
313     def src_builder(self):
314         return _null
315
316     def entry_abspath(self, name):
317         return name
318
319     def entry_path(self, name):
320         return name
321
322 # Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
323 _is_cygwin = sys.platform == "cygwin"
324 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
325     def _my_normcase(x):
326         return x
327 else:
328     def _my_normcase(x):
329         return string.upper(x)
330
331 class EntryProxy(SCons.Util.Proxy):
332     def __get_abspath(self):
333         entry = self.get()
334         return SCons.Util.SpecialAttrWrapper(entry.get_abspath(),
335                                              entry.name + "_abspath")
336
337     def __get_filebase(self):
338         name = self.get().name
339         return SCons.Util.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
340                                              name + "_filebase")
341
342     def __get_suffix(self):
343         name = self.get().name
344         return SCons.Util.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
345                                              name + "_suffix")
346
347     def __get_file(self):
348         name = self.get().name
349         return SCons.Util.SpecialAttrWrapper(name, name + "_file")
350
351     def __get_base_path(self):
352         """Return the file's directory and file name, with the
353         suffix stripped."""
354         entry = self.get()
355         return SCons.Util.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
356                                              entry.name + "_base")
357
358     def __get_posix_path(self):
359         """Return the path with / as the path separator,
360         regardless of platform."""
361         if os.sep == '/':
362             return self
363         else:
364             entry = self.get()
365             r = string.replace(entry.get_path(), os.sep, '/')
366             return SCons.Util.SpecialAttrWrapper(r, entry.name + "_posix")
367
368     def __get_win32_path(self):
369         """Return the path with \ as the path separator,
370         regardless of platform."""
371         if os.sep == '\\':
372             return self
373         else:
374             entry = self.get()
375             r = string.replace(entry.get_path(), os.sep, '\\')
376             return SCons.Util.SpecialAttrWrapper(r, entry.name + "_win32")
377
378     def __get_srcnode(self):
379         return EntryProxy(self.get().srcnode())
380
381     def __get_srcdir(self):
382         """Returns the directory containing the source node linked to this
383         node via BuildDir(), or the directory of this node if not linked."""
384         return EntryProxy(self.get().srcnode().dir)
385
386     def __get_rsrcnode(self):
387         return EntryProxy(self.get().srcnode().rfile())
388
389     def __get_rsrcdir(self):
390         """Returns the directory containing the source node linked to this
391         node via BuildDir(), or the directory of this node if not linked."""
392         return EntryProxy(self.get().srcnode().rfile().dir)
393
394     def __get_dir(self):
395         return EntryProxy(self.get().dir)
396     
397     dictSpecialAttrs = { "base"     : __get_base_path,
398                          "posix"    : __get_posix_path,
399                          "win32"    : __get_win32_path,
400                          "srcpath"  : __get_srcnode,
401                          "srcdir"   : __get_srcdir,
402                          "dir"      : __get_dir,
403                          "abspath"  : __get_abspath,
404                          "filebase" : __get_filebase,
405                          "suffix"   : __get_suffix,
406                          "file"     : __get_file,
407                          "rsrcpath" : __get_rsrcnode,
408                          "rsrcdir"  : __get_rsrcdir,
409                        }
410
411     def __getattr__(self, name):
412         # This is how we implement the "special" attributes
413         # such as base, posix, srcdir, etc.
414         try:
415             return self.dictSpecialAttrs[name](self)
416         except KeyError:
417             try:
418                 attr = SCons.Util.Proxy.__getattr__(self, name)
419             except AttributeError:
420                 entry = self.get()
421                 classname = string.split(str(entry.__class__), '.')[-1]
422                 if classname[-2:] == "'>":
423                     # new-style classes report their name as:
424                     #   "<class 'something'>"
425                     # instead of the classic classes:
426                     #   "something"
427                     classname = classname[:-2]
428                 raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
429             return attr
430
431 class Base(SCons.Node.Node):
432     """A generic class for file system entries.  This class is for
433     when we don't know yet whether the entry being looked up is a file
434     or a directory.  Instances of this class can morph into either
435     Dir or File objects by a later, more precise lookup.
436
437     Note: this class does not define __cmp__ and __hash__ for
438     efficiency reasons.  SCons does a lot of comparing of
439     Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
440     as fast as possible, which means we want to use Python's built-in
441     object identity comparisons.
442     """
443
444     def __init__(self, name, directory, fs):
445         """Initialize a generic Node.FS.Base object.
446         
447         Call the superclass initialization, take care of setting up
448         our relative and absolute paths, identify our parent
449         directory, and indicate that this node should use
450         signatures."""
451         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
452         SCons.Node.Node.__init__(self)
453
454         self.name = name
455         self.fs = fs
456
457         assert directory, "A directory must be provided"
458
459         self.abspath = directory.entry_abspath(name)
460         if directory.path == '.':
461             self.path = name
462         else:
463             self.path = directory.entry_path(name)
464
465         self.dir = directory
466         self.cwd = None # will hold the SConscript directory for target nodes
467         self.duplicate = directory.duplicate
468
469     def clear(self):
470         """Completely clear a Node.FS.Base object of all its cached
471         state (so that it can be re-evaluated by interfaces that do
472         continuous integration builds).
473         __cache_reset__
474         """
475         SCons.Node.Node.clear(self)
476
477     def get_dir(self):
478         return self.dir
479
480     def get_suffix(self):
481         "__cacheable__"
482         return SCons.Util.splitext(self.name)[1]
483
484     def rfile(self):
485         return self
486
487     def __str__(self):
488         """A Node.FS.Base object's string representation is its path
489         name."""
490         global Save_Strings
491         if Save_Strings:
492             return self._save_str()
493         return self._get_str()
494
495     def _save_str(self):
496         "__cacheable__"
497         return self._get_str()
498
499     def _get_str(self):
500         if self.duplicate or self.is_derived():
501             return self.get_path()
502         return self.srcnode().get_path()
503
504     rstr = __str__
505
506     def exists(self):
507         "__cacheable__"
508         return self.fs.exists(self.abspath)
509
510     def rexists(self):
511         "__cacheable__"
512         return self.rfile().exists()
513
514     def is_under(self, dir):
515         if self is dir:
516             return 1
517         else:
518             return self.dir.is_under(dir)
519
520     def set_local(self):
521         self._local = 1
522
523     def srcnode(self):
524         """If this node is in a build path, return the node
525         corresponding to its source file.  Otherwise, return
526         ourself.
527         __cacheable__"""
528         dir=self.dir
529         name=self.name
530         while dir:
531             if dir.srcdir:
532                 srcnode = self.fs.Entry(name, dir.srcdir,
533                                         klass=self.__class__)
534                 if srcnode.is_under(dir):
535                     # Shouldn't source from something in the build
536                     # path: probably means build_dir is under
537                     # src_dir and we are reflecting.
538                     break
539                 return srcnode
540             name = dir.name + os.sep + name
541             dir=dir.get_dir()
542         return self
543
544     def get_path(self, dir=None):
545         """Return path relative to the current working directory of the
546         Node.FS.Base object that owns us."""
547         if not dir:
548             dir = self.fs.getcwd()
549         path_elems = []
550         d = self
551         if d == dir:
552             path_elems.append('.')
553         else:
554             while d != dir and not isinstance(d, ParentOfRoot):
555                 path_elems.append(d.name)
556                 d = d.dir
557             path_elems.reverse()
558         ret = string.join(path_elems, os.sep)
559         return ret
560             
561     def set_src_builder(self, builder):
562         """Set the source code builder for this node."""
563         self.sbuilder = builder
564         if not self.has_builder():
565             self.builder_set(builder)
566
567     def src_builder(self):
568         """Fetch the source code builder for this node.
569
570         If there isn't one, we cache the source code builder specified
571         for the directory (which in turn will cache the value from its
572         parent directory, and so on up to the file system root).
573         """
574         try:
575             scb = self.sbuilder
576         except AttributeError:
577             scb = self.dir.src_builder()
578             self.sbuilder = scb
579         return scb
580
581     def get_abspath(self):
582         """Get the absolute path of the file."""
583         return self.abspath
584
585     def for_signature(self):
586         # Return just our name.  Even an absolute path would not work,
587         # because that can change thanks to symlinks or remapped network
588         # paths.
589         return self.name
590
591     def get_subst_proxy(self):
592         try:
593             return self._proxy
594         except AttributeError:
595             ret = EntryProxy(self)
596             self._proxy = ret
597             return ret
598
599 class Entry(Base):
600     """This is the class for generic Node.FS entries--that is, things
601     that could be a File or a Dir, but we're just not sure yet.
602     Consequently, the methods in this class really exist just to
603     transform their associated object into the right class when the
604     time comes, and then call the same-named method in the transformed
605     class."""
606
607     def rfile(self):
608         """We're a generic Entry, but the caller is actually looking for
609         a File at this point, so morph into one."""
610         self.__class__ = File
611         self._morph()
612         self.clear()
613         return File.rfile(self)
614
615     def get_found_includes(self, env, scanner, target):
616         """If we're looking for included files, it's because this Entry
617         is really supposed to be a File itself."""
618         node = self.rfile()
619         return node.get_found_includes(env, scanner, target)
620
621     def scanner_key(self):
622         return self.get_suffix()
623
624     def get_contents(self):
625         """Fetch the contents of the entry.
626         
627         Since this should return the real contents from the file
628         system, we check to see into what sort of subclass we should
629         morph this Entry."""
630         if self.fs.isfile(self.abspath):
631             self.__class__ = File
632             self._morph()
633             return self.get_contents()
634         if self.fs.isdir(self.abspath):
635             self.__class__ = Dir
636             self._morph()
637             return self.get_contents()
638         if self.fs.islink(self.abspath):
639             return ''             # avoid errors for dangling symlinks
640         raise AttributeError
641
642     def exists(self):
643         """Return if the Entry exists.  Check the file system to see
644         what we should turn into first.  Assume a file if there's no
645         directory."""
646         if self.fs.isdir(self.abspath):
647             self.__class__ = Dir
648             self._morph()
649             return Dir.exists(self)
650         else:
651             self.__class__ = File
652             self._morph()
653             self.clear()
654             return File.exists(self)
655
656     def calc_signature(self, calc=None):
657         """Return the Entry's calculated signature.  Check the file
658         system to see what we should turn into first.  Assume a file if
659         there's no directory."""
660         if self.fs.isdir(self.abspath):
661             self.__class__ = Dir
662             self._morph()
663             return Dir.calc_signature(self, calc)
664         else:
665             self.__class__ = File
666             self._morph()
667             self.clear()
668             return File.calc_signature(self, calc)
669
670     def must_be_a_Dir(self):
671         """Called to make sure a Node is a Dir.  Since we're an
672         Entry, we can morph into one."""
673         self.__class__ = Dir
674         self._morph()
675
676 # This is for later so we can differentiate between Entry the class and Entry
677 # the method of the FS class.
678 _classEntry = Entry
679
680
681 class LocalFS:
682
683     __metaclass__ = SCons.Memoize.Memoized_Metaclass
684     
685     # This class implements an abstraction layer for operations involving
686     # a local file system.  Essentially, this wraps any function in
687     # the os, os.path or shutil modules that we use to actually go do
688     # anything with or to the local file system.
689     #
690     # Note that there's a very good chance we'll refactor this part of
691     # the architecture in some way as we really implement the interface(s)
692     # for remote file system Nodes.  For example, the right architecture
693     # might be to have this be a subclass instead of a base class.
694     # Nevertheless, we're using this as a first step in that direction.
695     #
696     # We're not using chdir() yet because the calling subclass method
697     # needs to use os.chdir() directly to avoid recursion.  Will we
698     # really need this one?
699     #def chdir(self, path):
700     #    return os.chdir(path)
701     def chmod(self, path, mode):
702         return os.chmod(path, mode)
703     def copy2(self, src, dst):
704         return shutil.copy2(src, dst)
705     def exists(self, path):
706         return os.path.exists(path)
707     def getmtime(self, path):
708         return os.path.getmtime(path)
709     def isdir(self, path):
710         return os.path.isdir(path)
711     def isfile(self, path):
712         return os.path.isfile(path)
713     def link(self, src, dst):
714         return os.link(src, dst)
715     def listdir(self, path):
716         return os.listdir(path)
717     def makedirs(self, path):
718         return os.makedirs(path)
719     def mkdir(self, path):
720         return os.mkdir(path)
721     def rename(self, old, new):
722         return os.rename(old, new)
723     def stat(self, path):
724         return os.stat(path)
725     def symlink(self, src, dst):
726         return os.symlink(src, dst)
727     def open(self, path):
728         return open(path)
729     def unlink(self, path):
730         return os.unlink(path)
731
732     if hasattr(os, 'symlink'):
733         def islink(self, path):
734             return os.path.islink(path)
735         def exists_or_islink(self, path):
736             return os.path.exists(path) or os.path.islink(path)
737     else:
738         def islink(self, path):
739             return 0                    # no symlinks
740         exists_or_islink = exists
741
742 if not SCons.Memoize.has_metaclass:
743     _FSBase = LocalFS
744     class LocalFS(SCons.Memoize.Memoizer, _FSBase):
745         def __init__(self, *args, **kw):
746             apply(_FSBase.__init__, (self,)+args, kw)
747             SCons.Memoize.Memoizer.__init__(self)
748
749
750 #class RemoteFS:
751 #    # Skeleton for the obvious methods we might need from the
752 #    # abstraction layer for a remote filesystem.
753 #    def upload(self, local_src, remote_dst):
754 #        pass
755 #    def download(self, remote_src, local_dst):
756 #        pass
757
758
759 class FS(LocalFS):
760
761     def __init__(self, path = None):
762         """Initialize the Node.FS subsystem.
763
764         The supplied path is the top of the source tree, where we
765         expect to find the top-level build file.  If no path is
766         supplied, the current directory is the default.
767
768         The path argument must be a valid absolute path.
769         """
770         if __debug__: logInstanceCreation(self)
771         self.Top = None
772         if path == None:
773             self.pathTop = os.getcwd()
774         else:
775             self.pathTop = path
776         self.Root = {}
777         self.SConstruct_dir = None
778         self.CachePath = None
779         self.cache_force = None
780         self.cache_show = None
781
782     def set_toplevel_dir(self, path):
783         assert not self.Top, "You can only set the top-level path on an FS object that has not had its File, Dir, or Entry methods called yet."
784         self.pathTop = path
785
786     def clear_cache(self):
787         "__cache_reset__"
788         pass
789     
790     def set_SConstruct_dir(self, dir):
791         self.SConstruct_dir = dir
792         
793     def __setTopLevelDir(self):
794         if not self.Top:
795             self.Top = self.__doLookup(Dir, os.path.normpath(self.pathTop))
796             self.Top.path = '.'
797             self._cwd = self.Top
798         
799     def getcwd(self):
800         self.__setTopLevelDir()
801         return self._cwd
802
803     def __checkClass(self, node, klass):
804         if isinstance(node, klass) or klass == Entry:
805             return node
806         if node.__class__ == Entry:
807             node.__class__ = klass
808             node._morph()
809             return node
810         raise TypeError, "Tried to lookup %s '%s' as a %s." % \
811               (node.__class__.__name__, node.path, klass.__name__)
812         
813     def __doLookup(self, fsclass, name, directory = None, create = 1):
814         """This method differs from the File and Dir factory methods in
815         one important way: the meaning of the directory parameter.
816         In this method, if directory is None or not supplied, the supplied
817         name is expected to be an absolute path.  If you try to look up a
818         relative path with directory=None, then an AssertionError will be
819         raised.
820         __cacheable__"""
821
822         if not name:
823             # This is a stupid hack to compensate for the fact
824             # that the POSIX and Win32 versions of os.path.normpath()
825             # behave differently.  In particular, in POSIX:
826             #   os.path.normpath('./') == '.'
827             # in Win32
828             #   os.path.normpath('./') == ''
829             #   os.path.normpath('.\\') == ''
830             #
831             # This is a definite bug in the Python library, but we have
832             # to live with it.
833             name = '.'
834         path_comp = string.split(name, os.sep)
835         drive, path_first = os.path.splitdrive(path_comp[0])
836         if not path_first:
837             # Absolute path
838             drive = _my_normcase(drive)
839             try:
840                 directory = self.Root[drive]
841             except KeyError:
842                 if not create:
843                     raise SCons.Errors.UserError
844                 directory = RootDir(drive, ParentOfRoot(), self)
845                 self.Root[drive] = directory
846             path_comp = path_comp[1:]
847         else:
848             path_comp = [ path_first, ] + path_comp[1:]
849
850         if not path_comp:
851             path_comp = ['']
852             
853         # Lookup the directory
854         for path_name in path_comp[:-1]:
855             path_norm = _my_normcase(path_name)
856             try:
857                 d = directory.entries[path_norm]
858             except KeyError:
859                 if not create:
860                     raise SCons.Errors.UserError
861
862                 # look at the actual filesystem and make sure there isn't
863                 # a file already there
864                 path = directory.entry_path(path_name)
865                 if self.isfile(path):
866                     raise TypeError, \
867                           "File %s found where directory expected." % path
868
869                 dir_temp = Dir(path_name, directory, self)
870                 directory.entries[path_norm] = dir_temp
871                 directory.add_wkid(dir_temp)
872                 directory = dir_temp
873             else:
874                 d.must_be_a_Dir()
875                 directory = d
876
877         entry_norm = _my_normcase(path_comp[-1])
878         try:
879             e = directory.entries[entry_norm]
880         except KeyError:
881             if not create:
882                 raise SCons.Errors.UserError
883
884             # make sure we don't create File nodes when there is actually
885             # a directory at that path on the disk, and vice versa
886             path = directory.entry_path(path_comp[-1])
887             if fsclass == File:
888                 if self.isdir(path):
889                     raise TypeError, \
890                           "Directory %s found where file expected." % path
891             elif fsclass == Dir:
892                 if self.isfile(path):
893                     raise TypeError, \
894                           "File %s found where directory expected." % path
895             
896             result = fsclass(path_comp[-1], directory, self)
897             directory.entries[entry_norm] = result 
898             directory.add_wkid(result)
899         else:
900             result = self.__checkClass(e, fsclass)
901         return result 
902
903     def __transformPath(self, name, directory):
904         """Take care of setting up the correct top-level directory,
905         usually in preparation for a call to doLookup().
906
907         If the path name is prepended with a '#', then it is unconditionally
908         interpreted as relative to the top-level directory of this FS.
909
910         If directory is None, and name is a relative path,
911         then the same applies.
912         """
913         self.__setTopLevelDir()
914         if name and name[0] == '#':
915             directory = self.Top
916             name = name[1:]
917             if name and (name[0] == os.sep or name[0] == '/'):
918                 # Correct such that '#/foo' is equivalent
919                 # to '#foo'.
920                 name = name[1:]
921             name = os.path.join('.', os.path.normpath(name))
922         elif not directory:
923             directory = self._cwd
924         return (os.path.normpath(name), directory)
925
926     def chdir(self, dir, change_os_dir=0):
927         """Change the current working directory for lookups.
928         If change_os_dir is true, we will also change the "real" cwd
929         to match.
930         """
931         self.__setTopLevelDir()
932         curr=self._cwd
933         try:
934             if not dir is None:
935                 self._cwd = dir
936                 if change_os_dir:
937                     os.chdir(dir.abspath)
938         except OSError:
939             self._cwd = curr
940             raise
941
942     def Entry(self, name, directory = None, create = 1, klass=None):
943         """Lookup or create a generic Entry node with the specified name.
944         If the name is a relative path (begins with ./, ../, or a file
945         name), then it is looked up relative to the supplied directory
946         node, or to the top level directory of the FS (supplied at
947         construction time) if no directory is supplied.
948         """
949
950         if not klass:
951             klass = Entry
952
953         if isinstance(name, Base):
954             return self.__checkClass(name, klass)
955         else:
956             if directory and not isinstance(directory, Dir):
957                 directory = self.Dir(directory)
958             name, directory = self.__transformPath(name, directory)
959             return self.__doLookup(klass, name, directory, create)
960     
961     def File(self, name, directory = None, create = 1):
962         """Lookup or create a File node with the specified name.  If
963         the name is a relative path (begins with ./, ../, or a file name),
964         then it is looked up relative to the supplied directory node,
965         or to the top level directory of the FS (supplied at construction
966         time) if no directory is supplied.
967
968         This method will raise TypeError if a directory is found at the
969         specified path.
970         """
971
972         return self.Entry(name, directory, create, File)
973     
974     def Dir(self, name, directory = None, create = 1):
975         """Lookup or create a Dir node with the specified name.  If
976         the name is a relative path (begins with ./, ../, or a file name),
977         then it is looked up relative to the supplied directory node,
978         or to the top level directory of the FS (supplied at construction
979         time) if no directory is supplied.
980
981         This method will raise TypeError if a normal file is found at the
982         specified path.
983         """
984
985         return self.Entry(name, directory, create, Dir)
986     
987     def BuildDir(self, build_dir, src_dir, duplicate=1):
988         """Link the supplied build directory to the source directory
989         for purposes of building files."""
990         
991         self.__setTopLevelDir()
992         if not isinstance(src_dir, SCons.Node.Node):
993             src_dir = self.Dir(src_dir)
994         if not isinstance(build_dir, SCons.Node.Node):
995             build_dir = self.Dir(build_dir)
996         if not src_dir.is_under(self.Top):
997             raise SCons.Errors.UserError, "Source directory must be under top of build tree."
998         if src_dir.is_under(build_dir):
999             raise SCons.Errors.UserError, "Source directory cannot be under build directory."
1000         if build_dir.srcdir:
1001             if build_dir.srcdir == src_dir:
1002                 return # We already did this.
1003             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(build_dir, build_dir.srcdir)
1004         build_dir.link(src_dir, duplicate)
1005
1006     def Repository(self, *dirs):
1007         """Specify Repository directories to search."""
1008         for d in dirs:
1009             if not isinstance(d, SCons.Node.Node):
1010                 d = self.Dir(d)
1011             self.__setTopLevelDir()
1012             self.Top.addRepository(d)
1013
1014     def Rsearch(self, path, clazz=_classEntry, cwd=None):
1015         """Search for something in a Repository.  Returns the first
1016         one found in the list, or None if there isn't one.
1017         __cacheable__
1018         """
1019         if isinstance(path, SCons.Node.Node):
1020             return path
1021         else:
1022             name, d = self.__transformPath(path, cwd)
1023             n = self.__doLookup(clazz, name, d)
1024             if n.exists():
1025                 return n
1026             if isinstance(n, Dir):
1027                 # If n is a Directory that has Repositories directly
1028                 # attached to it, then any of those is a valid Repository
1029                 # path.  Return the first one that exists.
1030                 reps = filter(lambda x: x.exists(), n.getRepositories())
1031                 if len(reps):
1032                     return reps[0]
1033             d = n.get_dir()
1034             name = n.name
1035             # Search repositories of all directories that this file is under.
1036             while d:
1037                 for rep in d.getRepositories():
1038                     try:
1039                         rnode = self.__doLookup(clazz, name, rep)
1040                         # Only find the node if it exists and it is not
1041                         # a derived file.  If for some reason, we are
1042                         # explicitly building a file IN a Repository, we
1043                         # don't want it to show up in the build tree.
1044                         # This is usually the case with BuildDir().
1045                         # We only want to find pre-existing files.
1046                         if rnode.exists() and \
1047                            (isinstance(rnode, Dir) or not rnode.is_derived()):
1048                             return rnode
1049                     except TypeError:
1050                         pass # Wrong type of node.
1051                 # Prepend directory name
1052                 name = d.name + os.sep + name
1053                 # Go up one directory
1054                 d = d.get_dir()
1055         return None
1056
1057     def Rsearchall(self, pathlist, must_exist=1, clazz=_classEntry, cwd=None):
1058         """Search for a list of somethings in the Repository list.
1059         __cacheable__
1060         """
1061         ret = []
1062         if SCons.Util.is_String(pathlist):
1063             pathlist = string.split(pathlist, os.pathsep)
1064         if not SCons.Util.is_List(pathlist):
1065             pathlist = [pathlist]
1066         for path in filter(None, pathlist):
1067             if isinstance(path, SCons.Node.Node):
1068                 ret.append(path)
1069             else:
1070                 name, d = self.__transformPath(path, cwd)
1071                 n = self.__doLookup(clazz, name, d)
1072                 if not must_exist or n.exists():
1073                     ret.append(n)
1074                 if isinstance(n, Dir):
1075                     # If this node is a directory, then any repositories
1076                     # attached to this node can be repository paths.
1077                     ret.extend(filter(lambda x, me=must_exist, clazz=clazz: isinstance(x, clazz) and (not me or x.exists()),
1078                                       n.getRepositories()))
1079                     
1080                 d = n.get_dir()
1081                 name = n.name
1082                 # Search repositories of all directories that this file
1083                 # is under.
1084                 while d:
1085                     for rep in d.getRepositories():
1086                         try:
1087                             rnode = self.__doLookup(clazz, name, rep)
1088                             # Only find the node if it exists (or
1089                             # must_exist is zero) and it is not a
1090                             # derived file.  If for some reason, we
1091                             # are explicitly building a file IN a
1092                             # Repository, we don't want it to show up in
1093                             # the build tree.  This is usually the case
1094                             # with BuildDir().  We only want to find
1095                             # pre-existing files.
1096                             if (not must_exist or rnode.exists()) and \
1097                                (not rnode.is_derived() or isinstance(rnode, Dir)):
1098                                 ret.append(rnode)
1099                         except TypeError:
1100                             pass # Wrong type of node.
1101                     # Prepend directory name
1102                     name = d.name + os.sep + name
1103                     # Go up one directory
1104                     d = d.get_dir()
1105         return ret
1106
1107     def CacheDir(self, path):
1108         self.CachePath = path
1109
1110     def build_dir_target_climb(self, orig, dir, tail):
1111         """Create targets in corresponding build directories
1112
1113         Climb the directory tree, and look up path names
1114         relative to any linked build directories we find.
1115         __cacheable__
1116         """
1117         targets = []
1118         message = None
1119         fmt = "building associated BuildDir targets: %s"
1120         start_dir = dir
1121         while dir:
1122             for bd in dir.build_dirs:
1123                 if start_dir.is_under(bd):
1124                     # If already in the build-dir location, don't reflect
1125                     return [orig], fmt % str(orig)
1126                 p = apply(os.path.join, [bd.path] + tail)
1127                 targets.append(self.Entry(p))
1128             tail = [dir.name] + tail
1129             dir = dir.up()
1130         if targets:
1131             message = fmt % string.join(map(str, targets))
1132         return targets, message
1133
1134
1135 class Dir(Base):
1136     """A class for directories in a file system.
1137     """
1138
1139     def __init__(self, name, directory, fs):
1140         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1141         Base.__init__(self, name, directory, fs)
1142         self._morph()
1143
1144     def _morph(self):
1145         """Turn a file system Node (either a freshly initialized directory
1146         object or a separate Entry object) into a proper directory object.
1147
1148         Set up this directory's entries and hook it into the file
1149         system tree.  Specify that directories (this Node) don't use
1150         signatures for calculating whether they're current.
1151         __cache_reset__"""
1152
1153         self.repositories = []
1154         self.srcdir = None
1155
1156         self.entries = {}
1157         self.entries['.'] = self
1158         self.entries['..'] = self.dir
1159         self.cwd = self
1160         self.builder = get_MkdirBuilder()
1161         self.searched = 0
1162         self._sconsign = None
1163         self.build_dirs = []
1164
1165     def __clearRepositoryCache(self, duplicate=None):
1166         """Called when we change the repository(ies) for a directory.
1167         This clears any cached information that is invalidated by changing
1168         the repository."""
1169
1170         for node in self.entries.values():
1171             if node != self.dir:
1172                 if node != self and isinstance(node, Dir):
1173                     node.__clearRepositoryCache(duplicate)
1174                 else:
1175                     node.clear()
1176                     try:
1177                         del node._srcreps
1178                     except AttributeError:
1179                         pass
1180                     if duplicate != None:
1181                         node.duplicate=duplicate
1182     
1183     def __resetDuplicate(self, node):
1184         if node != self:
1185             node.duplicate = node.get_dir().duplicate
1186
1187     def Entry(self, name):
1188         """Create an entry node named 'name' relative to this directory."""
1189         return self.fs.Entry(name, self)
1190
1191     def Dir(self, name):
1192         """Create a directory node named 'name' relative to this directory."""
1193         return self.fs.Dir(name, self)
1194
1195     def File(self, name):
1196         """Create a file node named 'name' relative to this directory."""
1197         return self.fs.File(name, self)
1198
1199     def link(self, srcdir, duplicate):
1200         """Set this directory as the build directory for the
1201         supplied source directory."""
1202         self.srcdir = srcdir
1203         self.duplicate = duplicate
1204         self.__clearRepositoryCache(duplicate)
1205         srcdir.build_dirs.append(self)
1206
1207     def getRepositories(self):
1208         """Returns a list of repositories for this directory."""
1209         if self.srcdir and not self.duplicate:
1210             try:
1211                 return self._srcreps
1212             except AttributeError:
1213                 self._srcreps = self.fs.Rsearchall(self.srcdir.path,
1214                                                    clazz=Dir,
1215                                                    must_exist=0,
1216                                                    cwd=self.fs.Top) \
1217                                 + self.repositories
1218                 return self._srcreps
1219         return self.repositories
1220
1221     def addRepository(self, dir):
1222         if not dir in self.repositories and dir != self:
1223             self.repositories.append(dir)
1224             self.__clearRepositoryCache()
1225
1226     def up(self):
1227         return self.entries['..']
1228
1229     def root(self):
1230         if not self.entries['..']:
1231             return self
1232         else:
1233             return self.entries['..'].root()
1234
1235     def scan(self):
1236         if not self.implicit is None:
1237             return
1238         self.implicit = []
1239         self.implicit_dict = {}
1240         self._children_reset()
1241         try:
1242             for filename in self.fs.listdir(self.abspath):
1243                 if filename != '.sconsign':
1244                     self.Entry(filename)
1245         except OSError:
1246             # Directory does not exist.  No big deal
1247             pass
1248         keys = filter(lambda k: k != '.' and k != '..', self.entries.keys())
1249         kids = map(lambda x, s=self: s.entries[x], keys)
1250         def c(one, two):
1251             return cmp(one.abspath, two.abspath)
1252         kids.sort(c)
1253         self._add_child(self.implicit, self.implicit_dict, kids)
1254
1255     def build(self, **kw):
1256         """A null "builder" for directories."""
1257         global MkdirBuilder
1258         if not self.builder is MkdirBuilder:
1259             apply(SCons.Node.Node.build, [self,], kw)
1260
1261     def multiple_side_effect_has_builder(self):
1262         global MkdirBuilder
1263         return not self.builder is MkdirBuilder and self.has_builder()
1264
1265     def alter_targets(self):
1266         """Return any corresponding targets in a build directory.
1267         """
1268         return self.fs.build_dir_target_climb(self, self, [])
1269
1270     def scanner_key(self):
1271         """A directory does not get scanned."""
1272         return None
1273
1274     def get_contents(self):
1275         """Return aggregate contents of all our children."""
1276         contents = cStringIO.StringIO()
1277         for kid in self.children():
1278             contents.write(kid.get_contents())
1279         return contents.getvalue()
1280     
1281     def prepare(self):
1282         pass
1283
1284     def current(self, calc=None):
1285         """If all of our children were up-to-date, then this
1286         directory was up-to-date, too."""
1287         if not self.builder is MkdirBuilder and not self.exists():
1288             return 0
1289         state = 0
1290         for kid in self.children():
1291             s = kid.get_state()
1292             if s and (not state or s > state):
1293                 state = s
1294         import SCons.Node
1295         if state == 0 or state == SCons.Node.up_to_date:
1296             return 1
1297         else:
1298             return 0
1299
1300     def rdir(self):
1301         "__cacheable__"
1302         rdir = self
1303         if not self.exists():
1304             n = self.fs.Rsearch(self.path, clazz=Dir, cwd=self.fs.Top)
1305             if n:
1306                 rdir = n
1307         return rdir
1308
1309     def sconsign(self):
1310         """Return the .sconsign file info for this directory,
1311         creating it first if necessary."""
1312         if not self._sconsign:
1313             import SCons.SConsign
1314             self._sconsign = SCons.SConsign.ForDirectory(self)
1315         return self._sconsign
1316
1317     def srcnode(self):
1318         """Dir has a special need for srcnode()...if we
1319         have a srcdir attribute set, then that *is* our srcnode."""
1320         if self.srcdir:
1321             return self.srcdir
1322         return Base.srcnode(self)
1323
1324     def get_timestamp(self):
1325         """Return the latest timestamp from among our children"""
1326         stamp = 0
1327         for kid in self.children():
1328             if kid.get_timestamp() > stamp:
1329                 stamp = kid.get_timestamp()
1330         return stamp
1331
1332     def entry_abspath(self, name):
1333         return self.abspath + os.sep + name
1334
1335     def entry_path(self, name):
1336         return self.path + os.sep + name
1337
1338     def must_be_a_Dir(self):
1339         """Called to make sure a Node is a Dir.  Since we're already
1340         one, this is a no-op for us."""
1341         pass
1342
1343 class RootDir(Dir):
1344     """A class for the root directory of a file system.
1345
1346     This is the same as a Dir class, except that the path separator
1347     ('/' or '\\') is actually part of the name, so we don't need to
1348     add a separator when creating the path names of entries within
1349     this directory.
1350     """
1351     def __init__(self, name, directory, fs):
1352         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1353         Base.__init__(self, name, directory, fs)
1354         self.path = self.path + os.sep
1355         self.abspath = self.abspath + os.sep
1356         self._morph()
1357
1358     def entry_abspath(self, name):
1359         return self.abspath + name
1360
1361     def entry_path(self, name):
1362         return self.path + name
1363
1364 class BuildInfo:
1365     bsig = None
1366     def __cmp__(self, other):
1367         try:
1368             return cmp(self.bsig, other.bsig)
1369         except AttributeError:
1370             return 1
1371
1372 class File(Base):
1373     """A class for files in a file system.
1374     """
1375     def __init__(self, name, directory, fs):
1376         if __debug__: logInstanceCreation(self, 'Node.FS.File')
1377         Base.__init__(self, name, directory, fs)
1378         self._morph()
1379
1380     def Entry(self, name):
1381         """Create an entry node named 'name' relative to
1382         the SConscript directory of this file."""
1383         return self.fs.Entry(name, self.cwd)
1384
1385     def Dir(self, name):
1386         """Create a directory node named 'name' relative to
1387         the SConscript directory of this file."""
1388         return self.fs.Dir(name, self.cwd)
1389
1390     def File(self, name):
1391         """Create a file node named 'name' relative to
1392         the SConscript directory of this file."""
1393         return self.fs.File(name, self.cwd)
1394
1395     def RDirs(self, pathlist):
1396         """Search for a list of directories in the Repository list."""
1397         return self.fs.Rsearchall(pathlist, clazz=Dir, must_exist=0,
1398                                   cwd=self.cwd)
1399
1400     def _morph(self):
1401         """Turn a file system node into a File object.  __cache_reset__"""
1402         self.scanner_paths = {}
1403         if not hasattr(self, '_local'):
1404             self._local = 0
1405
1406     def root(self):
1407         return self.dir.root()
1408
1409     def scanner_key(self):
1410         return self.get_suffix()
1411
1412     def get_contents(self):
1413         if not self.rexists():
1414             return ''
1415         return open(self.rfile().abspath, "rb").read()
1416
1417     def get_timestamp(self):
1418         if self.rexists():
1419             return self.fs.getmtime(self.rfile().abspath)
1420         else:
1421             return 0
1422
1423     def store_info(self, obj):
1424         # Merge our build information into the already-stored entry.
1425         # This accomodates "chained builds" where a file that's a target
1426         # in one build (SConstruct file) is a source in a different build.
1427         # See test/chained-build.py for the use case.
1428         entry = self.get_stored_info()
1429         for key, val in obj.__dict__.items():
1430             entry.__dict__[key] = val
1431         self.dir.sconsign().set_entry(self.name, entry)
1432
1433     def get_stored_info(self):
1434         "__cacheable__"
1435         try:
1436             stored = self.dir.sconsign().get_entry(self.name)
1437         except (KeyError, OSError):
1438             return BuildInfo()
1439         else:
1440             if isinstance(stored, BuildInfo):
1441                 return stored
1442             # The stored build information isn't a BuildInfo object.
1443             # This probably means it's an old SConsignEntry from SCons
1444             # 0.95 or before.  The relevant attribute names are the same,
1445             # though, so just copy the attributes over to an object of
1446             # the correct type.
1447             binfo = BuildInfo()
1448             for key, val in stored.__dict__.items():
1449                 setattr(binfo, key, val)
1450             return binfo
1451
1452     def get_stored_implicit(self):
1453         binfo = self.get_stored_info()
1454         try:
1455             return binfo.bimplicit
1456         except AttributeError:
1457             return None
1458
1459     def get_found_includes(self, env, scanner, target):
1460         """Return the included implicit dependencies in this file.
1461         Cache results so we only scan the file once regardless of
1462         how many times this information is requested."""
1463         if not scanner:
1464             return []
1465
1466         try:
1467             path = target.scanner_paths[scanner]
1468         except AttributeError:
1469             # The target had no scanner_paths attribute, which means
1470             # it's an Alias or some other node that's not actually a
1471             # file.  In that case, back off and use the path for this
1472             # node itself.
1473             try:
1474                 path = self.scanner_paths[scanner]
1475             except KeyError:
1476                 path = scanner.path(env, self.cwd, target)
1477                 self.scanner_paths[scanner] = path
1478         except KeyError:
1479             path = scanner.path(env, target.cwd, target)
1480             target.scanner_paths[scanner] = path
1481
1482         return scanner(self, env, path)
1483
1484     def _createDir(self):
1485         # ensure that the directories for this node are
1486         # created.
1487
1488         listDirs = []
1489         parent=self.dir
1490         while parent:
1491             if parent.exists():
1492                 break
1493             listDirs.append(parent)
1494             p = parent.up()
1495             if isinstance(p, ParentOfRoot):
1496                 raise SCons.Errors.StopError, parent.path
1497             parent = p
1498         listDirs.reverse()
1499         for dirnode in listDirs:
1500             try:
1501                 # Don't call dirnode.build(), call the base Node method
1502                 # directly because we definitely *must* create this
1503                 # directory.  The dirnode.build() method will suppress
1504                 # the build if it's the default builder.
1505                 SCons.Node.Node.build(dirnode)
1506                 dirnode.get_executor().nullify()
1507                 # The build() action may or may not have actually
1508                 # created the directory, depending on whether the -n
1509                 # option was used or not.  Delete the _exists and
1510                 # _rexists attributes so they can be reevaluated.
1511                 dirnode.clear()
1512             except OSError:
1513                 pass
1514
1515     def retrieve_from_cache(self):
1516         """Try to retrieve the node's content from a cache
1517
1518         This method is called from multiple threads in a parallel build,
1519         so only do thread safe stuff here. Do thread unsafe stuff in
1520         built().
1521
1522         Note that there's a special trick here with the execute flag
1523         (one that's not normally done for other actions).  Basically
1524         if the user requested a noexec (-n) build, then
1525         SCons.Action.execute_actions is set to 0 and when any action
1526         is called, it does its showing but then just returns zero
1527         instead of actually calling the action execution operation.
1528         The problem for caching is that if the file does NOT exist in
1529         cache then the CacheRetrieveString won't return anything to
1530         show for the task, but the Action.__call__ won't call
1531         CacheRetrieveFunc; instead it just returns zero, which makes
1532         the code below think that the file *was* successfully
1533         retrieved from the cache, therefore it doesn't do any
1534         subsequent building.  However, the CacheRetrieveString didn't
1535         print anything because it didn't actually exist in the cache,
1536         and no more build actions will be performed, so the user just
1537         sees nothing.  The fix is to tell Action.__call__ to always
1538         execute the CacheRetrieveFunc and then have the latter
1539         explicitly check SCons.Action.execute_actions itself.
1540
1541         Returns true iff the node was successfully retrieved.
1542         """
1543         b = self.is_derived()
1544         if not b and not self.has_src_builder():
1545             return None
1546         if b and self.fs.CachePath:
1547             if self.fs.cache_show:
1548                 if CacheRetrieveSilent(self, [], None, execute=1) == 0:
1549                     self.build(presub=0, execute=0)
1550                     return 1
1551             elif CacheRetrieve(self, [], None, execute=1) == 0:
1552                 return 1
1553         return None
1554
1555     def built(self):
1556         """Called just after this node is successfully built.
1557         __cache_reset__"""
1558         # Push this file out to cache before the superclass Node.built()
1559         # method has a chance to clear the build signature, which it
1560         # will do if this file has a source scanner.
1561         if self.fs.CachePath and self.fs.exists(self.path):
1562             CachePush(self, [], None)
1563         self.fs.clear_cache()
1564         SCons.Node.Node.built(self)
1565
1566     def visited(self):
1567         if self.fs.CachePath and self.fs.cache_force and self.fs.exists(self.path):
1568             CachePush(self, None, None)
1569
1570     def has_src_builder(self):
1571         """Return whether this Node has a source builder or not.
1572
1573         If this Node doesn't have an explicit source code builder, this
1574         is where we figure out, on the fly, if there's a transparent
1575         source code builder for it.
1576
1577         Note that if we found a source builder, we also set the
1578         self.builder attribute, so that all of the methods that actually
1579         *build* this file don't have to do anything different.
1580         """
1581         try:
1582             scb = self.sbuilder
1583         except AttributeError:
1584             if self.rexists():
1585                 scb = None
1586             else:
1587                 scb = self.dir.src_builder()
1588                 if scb is _null:
1589                     scb = None
1590                     dir = self.dir.path
1591                     sccspath = os.path.join('SCCS', 's.' + self.name)
1592                     if dir != '.':
1593                         sccspath = os.path.join(dir, sccspath)
1594                     if self.fs.exists(sccspath):
1595                         scb = get_DefaultSCCSBuilder()
1596                     else:
1597                         rcspath = os.path.join('RCS', self.name + ',v')
1598                         if dir != '.':
1599                             rcspath = os.path.join(dir, rcspath)
1600                         if os.path.exists(rcspath):
1601                             scb = get_DefaultRCSBuilder()
1602                 if scb is not None:
1603                     self.builder_set(scb)
1604             self.sbuilder = scb
1605         return not scb is None
1606
1607     def alter_targets(self):
1608         """Return any corresponding targets in a build directory.
1609         """
1610         if self.is_derived():
1611             return [], None
1612         return self.fs.build_dir_target_climb(self, self.dir, [self.name])
1613
1614     def is_pseudo_derived(self):
1615         "__cacheable__"
1616         return self.has_src_builder()
1617
1618     def _rmv_existing(self):
1619         '__cache_reset__'
1620         Unlink(self, [], None)
1621         
1622     def prepare(self):
1623         """Prepare for this file to be created."""
1624         SCons.Node.Node.prepare(self)
1625
1626         if self.get_state() != SCons.Node.up_to_date:
1627             if self.exists():
1628                 if self.is_derived() and not self.precious:
1629                     self._rmv_existing()
1630             else:
1631                 try:
1632                     self._createDir()
1633                 except SCons.Errors.StopError, drive:
1634                     desc = "No drive `%s' for target `%s'." % (drive, self)
1635                     raise SCons.Errors.StopError, desc
1636
1637     def remove(self):
1638         """Remove this file."""
1639         if self.fs.exists_or_islink(self.path):
1640             self.fs.unlink(self.path)
1641             return 1
1642         return None
1643
1644     def exists(self):
1645         "__cacheable__"
1646         # Duplicate from source path if we are set up to do this.
1647         if self.duplicate and not self.is_derived() and not self.linked:
1648             src=self.srcnode()
1649             if src is self:
1650                 return Base.exists(self)
1651             src = src.rfile()
1652             if src.abspath != self.abspath and src.exists():
1653                 self._createDir()
1654                 try:
1655                     Unlink(self, None, None)
1656                 except SCons.Errors.BuildError:
1657                     pass
1658                 try:
1659                     Link(self, src, None)
1660                 except SCons.Errors.BuildError, e:
1661                     desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
1662                     raise SCons.Errors.StopError, desc
1663                 self.linked = 1
1664                 # The Link() action may or may not have actually
1665                 # created the file, depending on whether the -n
1666                 # option was used or not.  Delete the _exists and
1667                 # _rexists attributes so they can be reevaluated.
1668                 self.clear()
1669         return Base.exists(self)
1670
1671     def new_binfo(self):
1672         return BuildInfo()
1673
1674     def del_cinfo(self):
1675         try:
1676             del self.binfo.csig
1677         except AttributeError:
1678             pass
1679         try:
1680             del self.binfo.timestamp
1681         except AttributeError:
1682             pass
1683
1684     def calc_csig(self, calc=None):
1685         """
1686         Generate a node's content signature, the digested signature
1687         of its content.
1688
1689         node - the node
1690         cache - alternate node to use for the signature cache
1691         returns - the content signature
1692         """
1693         if calc is None:
1694             calc = self.calculator()
1695
1696         try:
1697             return self.binfo.csig
1698         except AttributeError:
1699             pass
1700         
1701         if calc.max_drift >= 0:
1702             old = self.get_stored_info()
1703         else:
1704             old = BuildInfo()
1705
1706         try:
1707             mtime = self.get_timestamp()
1708         except OSError:
1709             mtime = 0
1710             raise SCons.Errors.UserError, "no such %s" % self
1711
1712         try:
1713             if (old.timestamp and old.csig and old.timestamp == mtime):
1714                 # use the signature stored in the .sconsign file
1715                 csig = old.csig
1716             else:
1717                 csig = calc.module.signature(self)
1718         except AttributeError:
1719             csig = calc.module.signature(self)
1720
1721         if calc.max_drift >= 0 and (time.time() - mtime) > calc.max_drift:
1722             try:
1723                 binfo = self.binfo
1724             except AttributeError:
1725                 binfo = self.binfo = self.new_binfo()
1726             binfo.csig = csig
1727             binfo.timestamp = mtime
1728             self.store_info(binfo)
1729
1730         return csig
1731
1732     def current(self, calc=None):
1733         self.binfo = self.gen_binfo(calc)
1734         return self._cur2()
1735     def _cur2(self):
1736         "__cacheable__"
1737         if self.always_build:
1738             return None
1739         if not self.exists():
1740             # The file doesn't exist locally...
1741             r = self.rfile()
1742             if r != self:
1743                 # ...but there is one in a Repository...
1744                 old = r.get_stored_info()
1745                 if old == self.binfo:
1746                     # ...and it's even up-to-date...
1747                     if self._local:
1748                         # ...and they'd like a local copy.
1749                         LocalCopy(self, r, None)
1750                         self.store_info(self.binfo)
1751                     return 1
1752             return None
1753         else:
1754             old = self.get_stored_info()
1755             return (old == self.binfo)
1756
1757     def rfile(self):
1758         "__cacheable__"
1759         rfile = self
1760         if not self.exists():
1761             n = self.fs.Rsearch(self.path, clazz=File,
1762                                 cwd=self.fs.Top)
1763             if n:
1764                 rfile = n
1765         return rfile
1766
1767     def rstr(self):
1768         return str(self.rfile())
1769
1770     def cachepath(self):
1771         if not self.fs.CachePath:
1772             return None, None
1773         if self.binfo.bsig is None:
1774             raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
1775         # Add the path to the cache signature, because multiple
1776         # targets built by the same action will all have the same
1777         # build signature, and we have to differentiate them somehow.
1778         cache_sig = SCons.Sig.MD5.collect([self.binfo.bsig, self.path])
1779         subdir = string.upper(cache_sig[0])
1780         dir = os.path.join(self.fs.CachePath, subdir)
1781         return dir, os.path.join(dir, cache_sig)
1782
1783     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
1784         return self.dir.File(prefix + splitext(self.name)[0] + suffix)
1785
1786     def must_be_a_Dir(self):
1787         """Called to make sure a Node is a Dir.  Since we're already a
1788         File, this is a TypeError..."""
1789         raise TypeError, "Tried to lookup File '%s' as a Dir." % self.path
1790
1791 default_fs = FS()
1792
1793 def find_file(filename, paths, node_factory=default_fs.File, verbose=None):
1794     """
1795     find_file(str, [Dir()]) -> [nodes]
1796
1797     filename - a filename to find
1798     paths - a list of directory path *nodes* to search in.  Can be
1799             represented as a list, a tuple, or a callable that is
1800             called with no arguments and returns the list or tuple.
1801
1802     returns - the node created from the found file.
1803
1804     Find a node corresponding to either a derived file or a file
1805     that exists already.
1806
1807     Only the first file found is returned, and none is returned
1808     if no file is found.
1809     """
1810     if verbose and not SCons.Util.is_String(verbose):
1811         verbose = "find_file"
1812     retval = None
1813
1814     if callable(paths):
1815         paths = paths()
1816
1817     for dir in paths:
1818         if verbose:
1819             sys.stdout.write("  %s: looking for '%s' in '%s' ...\n" % (verbose, filename, dir))
1820         try:
1821             node = node_factory(filename, dir)
1822             # Return true if the node exists or is a derived node.
1823             if node.is_derived() or \
1824                node.is_pseudo_derived() or \
1825                (isinstance(node, SCons.Node.FS.Base) and node.exists()):
1826                 retval = node
1827                 if verbose:
1828                     sys.stdout.write("  %s: ... FOUND '%s' in '%s'\n" % (verbose, filename, dir))
1829                 break
1830         except TypeError:
1831             # If we find a directory instead of a file, we don't care
1832             pass
1833
1834     return retval
1835
1836 def find_files(filenames, paths, node_factory = default_fs.File):
1837     """
1838     find_files([str], [Dir()]) -> [nodes]
1839
1840     filenames - a list of filenames to find
1841     paths - a list of directory path *nodes* to search in
1842
1843     returns - the nodes created from the found files.
1844
1845     Finds nodes corresponding to either derived files or files
1846     that exist already.
1847
1848     Only the first file found is returned for each filename,
1849     and any files that aren't found are ignored.
1850     """
1851     nodes = map(lambda x, paths=paths, node_factory=node_factory:
1852                        find_file(x, paths, node_factory),
1853                 filenames)
1854     return filter(lambda x: x != None, nodes)