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