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