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