Refactor internals in preparation for eliminating creating unnecessary Node.FS objects.
[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 target[0].fs.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, path):
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, path)
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         return self
676
677 # This is for later so we can differentiate between Entry the class and Entry
678 # the method of the FS class.
679 _classEntry = Entry
680
681
682 class LocalFS:
683
684     __metaclass__ = SCons.Memoize.Memoized_Metaclass
685     
686     # This class implements an abstraction layer for operations involving
687     # a local file system.  Essentially, this wraps any function in
688     # the os, os.path or shutil modules that we use to actually go do
689     # anything with or to the local file system.
690     #
691     # Note that there's a very good chance we'll refactor this part of
692     # the architecture in some way as we really implement the interface(s)
693     # for remote file system Nodes.  For example, the right architecture
694     # might be to have this be a subclass instead of a base class.
695     # Nevertheless, we're using this as a first step in that direction.
696     #
697     # We're not using chdir() yet because the calling subclass method
698     # needs to use os.chdir() directly to avoid recursion.  Will we
699     # really need this one?
700     #def chdir(self, path):
701     #    return os.chdir(path)
702     def chmod(self, path, mode):
703         return os.chmod(path, mode)
704     def copy2(self, src, dst):
705         return shutil.copy2(src, dst)
706     def exists(self, path):
707         return os.path.exists(path)
708     def getmtime(self, path):
709         return os.path.getmtime(path)
710     def isdir(self, path):
711         return os.path.isdir(path)
712     def isfile(self, path):
713         return os.path.isfile(path)
714     def link(self, src, dst):
715         return os.link(src, dst)
716     def listdir(self, path):
717         return os.listdir(path)
718     def makedirs(self, path):
719         return os.makedirs(path)
720     def mkdir(self, path):
721         return os.mkdir(path)
722     def rename(self, old, new):
723         return os.rename(old, new)
724     def stat(self, path):
725         return os.stat(path)
726     def symlink(self, src, dst):
727         return os.symlink(src, dst)
728     def open(self, path):
729         return open(path)
730     def unlink(self, path):
731         return os.unlink(path)
732
733     if hasattr(os, 'symlink'):
734         def islink(self, path):
735             return os.path.islink(path)
736         def exists_or_islink(self, path):
737             return os.path.exists(path) or os.path.islink(path)
738     else:
739         def islink(self, path):
740             return 0                    # no symlinks
741         exists_or_islink = exists
742
743 if not SCons.Memoize.has_metaclass:
744     _FSBase = LocalFS
745     class LocalFS(SCons.Memoize.Memoizer, _FSBase):
746         def __init__(self, *args, **kw):
747             apply(_FSBase.__init__, (self,)+args, kw)
748             SCons.Memoize.Memoizer.__init__(self)
749
750
751 #class RemoteFS:
752 #    # Skeleton for the obvious methods we might need from the
753 #    # abstraction layer for a remote filesystem.
754 #    def upload(self, local_src, remote_dst):
755 #        pass
756 #    def download(self, remote_src, local_dst):
757 #        pass
758
759
760 class FS(LocalFS):
761
762     def __init__(self, path = None):
763         """Initialize the Node.FS subsystem.
764
765         The supplied path is the top of the source tree, where we
766         expect to find the top-level build file.  If no path is
767         supplied, the current directory is the default.
768
769         The path argument must be a valid absolute path.
770         """
771         if __debug__: logInstanceCreation(self)
772         self.Top = None
773         if path == None:
774             self.pathTop = os.getcwd()
775         else:
776             self.pathTop = path
777         self.Root = {}
778         self.SConstruct_dir = None
779         self.CachePath = None
780         self.cache_force = None
781         self.cache_show = None
782
783     def set_toplevel_dir(self, path):
784         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."
785         self.pathTop = path
786
787     def clear_cache(self):
788         "__cache_reset__"
789         pass
790     
791     def set_SConstruct_dir(self, dir):
792         self.SConstruct_dir = dir
793         
794     def __setTopLevelDir(self):
795         if not self.Top:
796             self.Top = self._doLookup(Dir, os.path.normpath(self.pathTop))
797             self.Top.path = '.'
798             self._cwd = self.Top
799         
800     def getcwd(self):
801         self.__setTopLevelDir()
802         return self._cwd
803
804     def __checkClass(self, node, klass):
805         if isinstance(node, klass) or klass == Entry:
806             return node
807         if node.__class__ == Entry:
808             node.__class__ = klass
809             node._morph()
810             return node
811         raise TypeError, "Tried to lookup %s '%s' as a %s." % \
812               (node.__class__.__name__, node.path, klass.__name__)
813         
814     def _doLookup(self, fsclass, name, directory = None, create = 1):
815         """This method differs from the File and Dir factory methods in
816         one important way: the meaning of the directory parameter.
817         In this method, if directory is None or not supplied, the supplied
818         name is expected to be an absolute path.  If you try to look up a
819         relative path with directory=None, then an AssertionError will be
820         raised.
821         __cacheable__"""
822
823         if not name:
824             # This is a stupid hack to compensate for the fact
825             # that the POSIX and Win32 versions of os.path.normpath()
826             # behave differently.  In particular, in POSIX:
827             #   os.path.normpath('./') == '.'
828             # in Win32
829             #   os.path.normpath('./') == ''
830             #   os.path.normpath('.\\') == ''
831             #
832             # This is a definite bug in the Python library, but we have
833             # to live with it.
834             name = '.'
835         path_comp = string.split(name, os.sep)
836         drive, path_first = os.path.splitdrive(path_comp[0])
837         if not path_first:
838             # Absolute path
839             drive = _my_normcase(drive)
840             try:
841                 directory = self.Root[drive]
842             except KeyError:
843                 if not create:
844                     raise SCons.Errors.UserError
845                 directory = RootDir(drive, ParentOfRoot(), self)
846                 self.Root[drive] = directory
847             path_comp = path_comp[1:]
848         else:
849             path_comp = [ path_first, ] + path_comp[1:]
850
851         if not path_comp:
852             path_comp = ['']
853             
854         # Lookup the directory
855         for path_name in path_comp[:-1]:
856             path_norm = _my_normcase(path_name)
857             try:
858                 d = directory.entries[path_norm]
859             except KeyError:
860                 if not create:
861                     raise SCons.Errors.UserError
862
863                 # look at the actual filesystem and make sure there isn't
864                 # a file already there
865                 path = directory.entry_path(path_name)
866                 if self.isfile(path):
867                     raise TypeError, \
868                           "File %s found where directory expected." % path
869
870                 dir_temp = Dir(path_name, directory, self)
871                 directory.entries[path_norm] = dir_temp
872                 directory.add_wkid(dir_temp)
873                 directory = dir_temp
874             else:
875                 directory = d.must_be_a_Dir()
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 do_duplicate(self, src):
1285         pass
1286
1287     def current(self, calc=None):
1288         """If all of our children were up-to-date, then this
1289         directory was up-to-date, too."""
1290         if not self.builder is MkdirBuilder and not self.exists():
1291             return 0
1292         state = 0
1293         for kid in self.children():
1294             s = kid.get_state()
1295             if s and (not state or s > state):
1296                 state = s
1297         import SCons.Node
1298         if state == 0 or state == SCons.Node.up_to_date:
1299             return 1
1300         else:
1301             return 0
1302
1303     def rdir(self):
1304         "__cacheable__"
1305         rdir = self
1306         if not self.exists():
1307             n = self.fs.Rsearch(self.path, clazz=Dir, cwd=self.fs.Top)
1308             if n:
1309                 rdir = n
1310         return rdir
1311
1312     def sconsign(self):
1313         """Return the .sconsign file info for this directory,
1314         creating it first if necessary."""
1315         if not self._sconsign:
1316             import SCons.SConsign
1317             self._sconsign = SCons.SConsign.ForDirectory(self)
1318         return self._sconsign
1319
1320     def srcnode(self):
1321         """Dir has a special need for srcnode()...if we
1322         have a srcdir attribute set, then that *is* our srcnode."""
1323         if self.srcdir:
1324             return self.srcdir
1325         return Base.srcnode(self)
1326
1327     def get_timestamp(self):
1328         """Return the latest timestamp from among our children"""
1329         stamp = 0
1330         for kid in self.children():
1331             if kid.get_timestamp() > stamp:
1332                 stamp = kid.get_timestamp()
1333         return stamp
1334
1335     def entry_abspath(self, name):
1336         return self.abspath + os.sep + name
1337
1338     def entry_path(self, name):
1339         return self.path + os.sep + name
1340
1341     def must_be_a_Dir(self):
1342         """Called to make sure a Node is a Dir.  Since we're already
1343         one, this is a no-op for us."""
1344         return self
1345
1346     def entry_exists_on_disk(self, name):
1347         """__cacheable__"""
1348         return self.fs.exists(self.entry_abspath(name))
1349
1350     def rcs_on_disk(self, name):
1351         rcspath = 'RCS' + os.sep + name+',v'
1352         return self.entry_exists_on_disk(rcspath)
1353
1354     def sccs_on_disk(self, name):
1355         sccspath = 'SCCS' + os.sep + 's.'+name
1356         return self.entry_exists_on_disk(sccspath)
1357
1358 class RootDir(Dir):
1359     """A class for the root directory of a file system.
1360
1361     This is the same as a Dir class, except that the path separator
1362     ('/' or '\\') is actually part of the name, so we don't need to
1363     add a separator when creating the path names of entries within
1364     this directory.
1365     """
1366     def __init__(self, name, directory, fs):
1367         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1368         Base.__init__(self, name, directory, fs)
1369         self.path = self.path + os.sep
1370         self.abspath = self.abspath + os.sep
1371         self._morph()
1372
1373     def entry_abspath(self, name):
1374         return self.abspath + name
1375
1376     def entry_path(self, name):
1377         return self.path + name
1378
1379 class BuildInfo:
1380     bsig = None
1381     def __cmp__(self, other):
1382         try:
1383             return cmp(self.bsig, other.bsig)
1384         except AttributeError:
1385             return 1
1386
1387 class File(Base):
1388     """A class for files in a file system.
1389     """
1390     def __init__(self, name, directory, fs):
1391         if __debug__: logInstanceCreation(self, 'Node.FS.File')
1392         Base.__init__(self, name, directory, fs)
1393         self._morph()
1394
1395     def Entry(self, name):
1396         """Create an entry node named 'name' relative to
1397         the SConscript directory of this file."""
1398         return self.fs.Entry(name, self.cwd)
1399
1400     def Dir(self, name):
1401         """Create a directory node named 'name' relative to
1402         the SConscript directory of this file."""
1403         return self.fs.Dir(name, self.cwd)
1404
1405     def File(self, name):
1406         """Create a file node named 'name' relative to
1407         the SConscript directory of this file."""
1408         return self.fs.File(name, self.cwd)
1409
1410     def RDirs(self, pathlist):
1411         """Search for a list of directories in the Repository list."""
1412         return self.fs.Rsearchall(pathlist, clazz=Dir, must_exist=0,
1413                                   cwd=self.cwd)
1414
1415     def _morph(self):
1416         """Turn a file system node into a File object.  __cache_reset__"""
1417         self.scanner_paths = {}
1418         if not hasattr(self, '_local'):
1419             self._local = 0
1420
1421     def root(self):
1422         return self.dir.root()
1423
1424     def scanner_key(self):
1425         return self.get_suffix()
1426
1427     def get_contents(self):
1428         if not self.rexists():
1429             return ''
1430         return open(self.rfile().abspath, "rb").read()
1431
1432     def get_timestamp(self):
1433         if self.rexists():
1434             return self.fs.getmtime(self.rfile().abspath)
1435         else:
1436             return 0
1437
1438     def store_info(self, obj):
1439         # Merge our build information into the already-stored entry.
1440         # This accomodates "chained builds" where a file that's a target
1441         # in one build (SConstruct file) is a source in a different build.
1442         # See test/chained-build.py for the use case.
1443         entry = self.get_stored_info()
1444         for key, val in obj.__dict__.items():
1445             entry.__dict__[key] = val
1446         self.dir.sconsign().set_entry(self.name, entry)
1447
1448     def get_stored_info(self):
1449         "__cacheable__"
1450         try:
1451             stored = self.dir.sconsign().get_entry(self.name)
1452         except (KeyError, OSError):
1453             return BuildInfo()
1454         else:
1455             if isinstance(stored, BuildInfo):
1456                 return stored
1457             # The stored build information isn't a BuildInfo object.
1458             # This probably means it's an old SConsignEntry from SCons
1459             # 0.95 or before.  The relevant attribute names are the same,
1460             # though, so just copy the attributes over to an object of
1461             # the correct type.
1462             binfo = BuildInfo()
1463             for key, val in stored.__dict__.items():
1464                 setattr(binfo, key, val)
1465             return binfo
1466
1467     def get_stored_implicit(self):
1468         binfo = self.get_stored_info()
1469         try:
1470             return binfo.bimplicit
1471         except AttributeError:
1472             return None
1473
1474     def get_found_includes(self, env, scanner, path):
1475         """Return the included implicit dependencies in this file.
1476         Cache results so we only scan the file once per path
1477         regardless of how many times this information is requested.
1478         __cacheable__"""
1479         if not scanner:
1480             return []
1481         return scanner(self, env, path)
1482
1483     def _createDir(self):
1484         # ensure that the directories for this node are
1485         # created.
1486
1487         listDirs = []
1488         parent=self.dir
1489         while parent:
1490             if parent.exists():
1491                 break
1492             listDirs.append(parent)
1493             p = parent.up()
1494             if isinstance(p, ParentOfRoot):
1495                 raise SCons.Errors.StopError, parent.path
1496             parent = p
1497         listDirs.reverse()
1498         for dirnode in listDirs:
1499             try:
1500                 # Don't call dirnode.build(), call the base Node method
1501                 # directly because we definitely *must* create this
1502                 # directory.  The dirnode.build() method will suppress
1503                 # the build if it's the default builder.
1504                 SCons.Node.Node.build(dirnode)
1505                 dirnode.get_executor().nullify()
1506                 # The build() action may or may not have actually
1507                 # created the directory, depending on whether the -n
1508                 # option was used or not.  Delete the _exists and
1509                 # _rexists attributes so they can be reevaluated.
1510                 dirnode.clear()
1511             except OSError:
1512                 pass
1513
1514     def retrieve_from_cache(self):
1515         """Try to retrieve the node's content from a cache
1516
1517         This method is called from multiple threads in a parallel build,
1518         so only do thread safe stuff here. Do thread unsafe stuff in
1519         built().
1520
1521         Note that there's a special trick here with the execute flag
1522         (one that's not normally done for other actions).  Basically
1523         if the user requested a noexec (-n) build, then
1524         SCons.Action.execute_actions is set to 0 and when any action
1525         is called, it does its showing but then just returns zero
1526         instead of actually calling the action execution operation.
1527         The problem for caching is that if the file does NOT exist in
1528         cache then the CacheRetrieveString won't return anything to
1529         show for the task, but the Action.__call__ won't call
1530         CacheRetrieveFunc; instead it just returns zero, which makes
1531         the code below think that the file *was* successfully
1532         retrieved from the cache, therefore it doesn't do any
1533         subsequent building.  However, the CacheRetrieveString didn't
1534         print anything because it didn't actually exist in the cache,
1535         and no more build actions will be performed, so the user just
1536         sees nothing.  The fix is to tell Action.__call__ to always
1537         execute the CacheRetrieveFunc and then have the latter
1538         explicitly check SCons.Action.execute_actions itself.
1539
1540         Returns true iff the node was successfully retrieved.
1541         """
1542         b = self.is_derived()
1543         if not b and not self.has_src_builder():
1544             return None
1545         if b and self.fs.CachePath:
1546             if self.fs.cache_show:
1547                 if CacheRetrieveSilent(self, [], None, execute=1) == 0:
1548                     self.build(presub=0, execute=0)
1549                     return 1
1550             elif CacheRetrieve(self, [], None, execute=1) == 0:
1551                 return 1
1552         return None
1553
1554     def built(self):
1555         """Called just after this node is successfully built.
1556         __cache_reset__"""
1557         # Push this file out to cache before the superclass Node.built()
1558         # method has a chance to clear the build signature, which it
1559         # will do if this file has a source scanner.
1560         if self.fs.CachePath and self.fs.exists(self.path):
1561             CachePush(self, [], None)
1562         self.fs.clear_cache()
1563         SCons.Node.Node.built(self)
1564
1565     def visited(self):
1566         if self.fs.CachePath and self.fs.cache_force and self.fs.exists(self.path):
1567             CachePush(self, None, None)
1568
1569     def has_src_builder(self):
1570         """Return whether this Node has a source builder or not.
1571
1572         If this Node doesn't have an explicit source code builder, this
1573         is where we figure out, on the fly, if there's a transparent
1574         source code builder for it.
1575
1576         Note that if we found a source builder, we also set the
1577         self.builder attribute, so that all of the methods that actually
1578         *build* this file don't have to do anything different.
1579         """
1580         try:
1581             scb = self.sbuilder
1582         except AttributeError:
1583             if self.rexists():
1584                 scb = None
1585             else:
1586                 scb = self.dir.src_builder()
1587                 if scb is _null:
1588                     if self.dir.sccs_on_disk(self.name):
1589                         scb = get_DefaultSCCSBuilder()
1590                     elif self.dir.rcs_on_disk(self.name):
1591                         scb = get_DefaultRCSBuilder()
1592                     else:
1593                         scb = None
1594                 if scb is not None:
1595                     self.builder_set(scb)
1596             self.sbuilder = scb
1597         return not scb is None
1598
1599     def alter_targets(self):
1600         """Return any corresponding targets in a build directory.
1601         """
1602         if self.is_derived():
1603             return [], None
1604         return self.fs.build_dir_target_climb(self, self.dir, [self.name])
1605
1606     def is_pseudo_derived(self):
1607         "__cacheable__"
1608         return self.has_src_builder()
1609
1610     def _rmv_existing(self):
1611         '__cache_reset__'
1612         Unlink(self, [], None)
1613         
1614     def prepare(self):
1615         """Prepare for this file to be created."""
1616         SCons.Node.Node.prepare(self)
1617
1618         if self.get_state() != SCons.Node.up_to_date:
1619             if self.exists():
1620                 if self.is_derived() and not self.precious:
1621                     self._rmv_existing()
1622             else:
1623                 try:
1624                     self._createDir()
1625                 except SCons.Errors.StopError, drive:
1626                     desc = "No drive `%s' for target `%s'." % (drive, self)
1627                     raise SCons.Errors.StopError, desc
1628
1629     def remove(self):
1630         """Remove this file."""
1631         if self.fs.exists_or_islink(self.path):
1632             self.fs.unlink(self.path)
1633             return 1
1634         return None
1635
1636     def do_duplicate(self, src):
1637         self._createDir()
1638         try:
1639             Unlink(self, None, None)
1640         except SCons.Errors.BuildError:
1641             pass
1642         try:
1643             Link(self, src, None)
1644         except SCons.Errors.BuildError, e:
1645             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
1646             raise SCons.Errors.StopError, desc
1647         self.linked = 1
1648         # The Link() action may or may not have actually
1649         # created the file, depending on whether the -n
1650         # option was used or not.  Delete the _exists and
1651         # _rexists attributes so they can be reevaluated.
1652         self.clear()
1653
1654     def exists(self):
1655         "__cacheable__"
1656         # Duplicate from source path if we are set up to do this.
1657         if self.duplicate and not self.is_derived() and not self.linked:
1658             src=self.srcnode()
1659             if src is self:
1660                 return Base.exists(self)
1661             src = src.rfile()
1662             if src.abspath != self.abspath and src.exists():
1663                 self.do_duplicate(src)
1664         return Base.exists(self)
1665
1666     def new_binfo(self):
1667         return BuildInfo()
1668
1669     def del_cinfo(self):
1670         try:
1671             del self.binfo.csig
1672         except AttributeError:
1673             pass
1674         try:
1675             del self.binfo.timestamp
1676         except AttributeError:
1677             pass
1678
1679     def calc_csig(self, calc=None):
1680         """
1681         Generate a node's content signature, the digested signature
1682         of its content.
1683
1684         node - the node
1685         cache - alternate node to use for the signature cache
1686         returns - the content signature
1687         """
1688         if calc is None:
1689             calc = self.calculator()
1690
1691         try:
1692             return self.binfo.csig
1693         except AttributeError:
1694             pass
1695         
1696         if calc.max_drift >= 0:
1697             old = self.get_stored_info()
1698         else:
1699             old = BuildInfo()
1700
1701         try:
1702             mtime = self.get_timestamp()
1703         except OSError:
1704             mtime = 0
1705             raise SCons.Errors.UserError, "no such %s" % self
1706
1707         try:
1708             if (old.timestamp and old.csig and old.timestamp == mtime):
1709                 # use the signature stored in the .sconsign file
1710                 csig = old.csig
1711             else:
1712                 csig = calc.module.signature(self)
1713         except AttributeError:
1714             csig = calc.module.signature(self)
1715
1716         if calc.max_drift >= 0 and (time.time() - mtime) > calc.max_drift:
1717             try:
1718                 binfo = self.binfo
1719             except AttributeError:
1720                 binfo = self.binfo = self.new_binfo()
1721             binfo.csig = csig
1722             binfo.timestamp = mtime
1723             self.store_info(binfo)
1724
1725         return csig
1726
1727     def current(self, calc=None):
1728         self.binfo = self.gen_binfo(calc)
1729         return self._cur2()
1730     def _cur2(self):
1731         "__cacheable__"
1732         if self.always_build:
1733             return None
1734         if not self.exists():
1735             # The file doesn't exist locally...
1736             r = self.rfile()
1737             if r != self:
1738                 # ...but there is one in a Repository...
1739                 old = r.get_stored_info()
1740                 if old == self.binfo:
1741                     # ...and it's even up-to-date...
1742                     if self._local:
1743                         # ...and they'd like a local copy.
1744                         LocalCopy(self, r, None)
1745                         self.store_info(self.binfo)
1746                     return 1
1747             return None
1748         else:
1749             old = self.get_stored_info()
1750             return (old == self.binfo)
1751
1752     def rfile(self):
1753         "__cacheable__"
1754         rfile = self
1755         if not self.exists():
1756             n = self.fs.Rsearch(self.path, clazz=File,
1757                                 cwd=self.fs.Top)
1758             if n:
1759                 rfile = n
1760         return rfile
1761
1762     def rstr(self):
1763         return str(self.rfile())
1764
1765     def cachepath(self):
1766         if not self.fs.CachePath:
1767             return None, None
1768         if self.binfo.bsig is None:
1769             raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
1770         # Add the path to the cache signature, because multiple
1771         # targets built by the same action will all have the same
1772         # build signature, and we have to differentiate them somehow.
1773         cache_sig = SCons.Sig.MD5.collect([self.binfo.bsig, self.path])
1774         subdir = string.upper(cache_sig[0])
1775         dir = os.path.join(self.fs.CachePath, subdir)
1776         return dir, os.path.join(dir, cache_sig)
1777
1778     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
1779         return self.dir.File(prefix + splitext(self.name)[0] + suffix)
1780
1781     def must_be_a_Dir(self):
1782         """Called to make sure a Node is a Dir.  Since we're already a
1783         File, this is a TypeError..."""
1784         raise TypeError, "Tried to lookup File '%s' as a Dir." % self.path
1785
1786 default_fs = FS()
1787
1788 def find_file(filename, paths, node_factory=default_fs.File, verbose=None):
1789     """
1790     find_file(str, [Dir()]) -> [nodes]
1791
1792     filename - a filename to find
1793     paths - a list of directory path *nodes* to search in.  Can be
1794             represented as a list, a tuple, or a callable that is
1795             called with no arguments and returns the list or tuple.
1796
1797     returns - the node created from the found file.
1798
1799     Find a node corresponding to either a derived file or a file
1800     that exists already.
1801
1802     Only the first file found is returned, and none is returned
1803     if no file is found.
1804     """
1805     if verbose:
1806         if not SCons.Util.is_String(verbose):
1807             verbose = "find_file"
1808         if not callable(verbose):
1809             verbose = '  %s: ' % verbose
1810             verbose = lambda s, v=verbose: sys.stdout.write(v + s)
1811     else:
1812         verbose = lambda x: x
1813
1814     retval = None
1815
1816     if callable(paths):
1817         paths = paths()
1818
1819     for dir in paths:
1820         verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
1821         try:
1822             node = node_factory(filename, dir)
1823             # Return true if the node exists or is a derived node.
1824             if node.is_derived() or \
1825                node.is_pseudo_derived() or \
1826                (isinstance(node, SCons.Node.FS.Base) and node.exists()):
1827                 retval = node
1828                 verbose("... FOUND '%s' in '%s'\n" % (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)