Have Visual Studio echo that we're using SCons to build things, to work around a...
[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             drive = _my_normcase(drive)
906             # Absolute path
907             try:
908                 directory = self.Root[drive]
909             except KeyError:
910                 if not create:
911                     raise SCons.Errors.UserError
912                 directory = RootDir(drive, self)
913                 self.Root[drive] = directory
914
915         if not path_orig:
916             return directory
917
918         last_orig = path_orig.pop()     # strip last element
919         last_norm = path_norm.pop()     # strip last element
920             
921         # Lookup the directory
922         for orig, norm in map(None, path_orig, path_norm):
923             try:
924                 entries = directory.entries
925             except AttributeError:
926                 # We tried to look up the entry in either an Entry or
927                 # a File.  Give whatever it is a chance to do what's
928                 # appropriate: morph into a Dir or raise an exception.
929                 directory.must_be_a_Dir()
930                 entries = directory.entries
931             try:
932                 directory = entries[norm]
933             except KeyError:
934                 if not create:
935                     raise SCons.Errors.UserError
936
937                 d = Dir(orig, directory, self)
938
939                 # Check the file system (or not, as configured) to make
940                 # sure there isn't already a file there.
941                 d.diskcheck_match()
942
943                 directory.entries[norm] = d
944                 directory.add_wkid(d)
945                 directory = d
946
947         directory.must_be_a_Dir()
948
949         try:
950             e = directory.entries[last_norm]
951         except KeyError:
952             if not create:
953                 raise SCons.Errors.UserError
954
955             result = fsclass(last_orig, directory, self)
956
957             # Check the file system (or not, as configured) to make
958             # sure there isn't already a directory at the path on
959             # disk where we just created a File node, and vice versa.
960             result.diskcheck_match()
961
962             directory.entries[last_norm] = result 
963             directory.add_wkid(result)
964         else:
965             result = self.__checkClass(e, fsclass)
966         return result 
967
968     def _transformPath(self, name, directory):
969         """Take care of setting up the correct top-level directory,
970         usually in preparation for a call to doLookup().
971
972         If the path name is prepended with a '#', then it is unconditionally
973         interpreted as relative to the top-level directory of this FS.
974
975         If directory is None, and name is a relative path,
976         then the same applies.
977         """
978         if name and name[0] == '#':
979             directory = self.Top
980             name = name[1:]
981             if name and (name[0] == os.sep or name[0] == '/'):
982                 # Correct such that '#/foo' is equivalent
983                 # to '#foo'.
984                 name = name[1:]
985             name = os.path.join('.', os.path.normpath(name))
986         elif not directory:
987             directory = self._cwd
988         return (os.path.normpath(name), directory)
989
990     def chdir(self, dir, change_os_dir=0):
991         """Change the current working directory for lookups.
992         If change_os_dir is true, we will also change the "real" cwd
993         to match.
994         """
995         curr=self._cwd
996         try:
997             if not dir is None:
998                 self._cwd = dir
999                 if change_os_dir:
1000                     os.chdir(dir.abspath)
1001         except OSError:
1002             self._cwd = curr
1003             raise
1004
1005     def Entry(self, name, directory = None, create = 1, klass=None):
1006         """Lookup or create a generic Entry node with the specified name.
1007         If the name is a relative path (begins with ./, ../, or a file
1008         name), then it is looked up relative to the supplied directory
1009         node, or to the top level directory of the FS (supplied at
1010         construction time) if no directory is supplied.
1011         """
1012
1013         if not klass:
1014             klass = Entry
1015
1016         if isinstance(name, Base):
1017             return self.__checkClass(name, klass)
1018         else:
1019             if directory and not isinstance(directory, Dir):
1020                 directory = self.Dir(directory)
1021             name, directory = self._transformPath(name, directory)
1022             return self._doLookup(klass, name, directory, create)
1023     
1024     def File(self, name, directory = None, create = 1):
1025         """Lookup or create a File node with the specified name.  If
1026         the name is a relative path (begins with ./, ../, or a file name),
1027         then it is looked up relative to the supplied directory node,
1028         or to the top level directory of the FS (supplied at construction
1029         time) if no directory is supplied.
1030
1031         This method will raise TypeError if a directory is found at the
1032         specified path.
1033         """
1034
1035         return self.Entry(name, directory, create, File)
1036     
1037     def Dir(self, name, directory = None, create = 1):
1038         """Lookup or create a Dir node with the specified name.  If
1039         the name is a relative path (begins with ./, ../, or a file name),
1040         then it is looked up relative to the supplied directory node,
1041         or to the top level directory of the FS (supplied at construction
1042         time) if no directory is supplied.
1043
1044         This method will raise TypeError if a normal file is found at the
1045         specified path.
1046         """
1047
1048         return self.Entry(name, directory, create, Dir)
1049     
1050     def BuildDir(self, build_dir, src_dir, duplicate=1):
1051         """Link the supplied build directory to the source directory
1052         for purposes of building files."""
1053         
1054         if not isinstance(src_dir, SCons.Node.Node):
1055             src_dir = self.Dir(src_dir)
1056         if not isinstance(build_dir, SCons.Node.Node):
1057             build_dir = self.Dir(build_dir)
1058         if src_dir.is_under(build_dir):
1059             raise SCons.Errors.UserError, "Source directory cannot be under build directory."
1060         if build_dir.srcdir:
1061             if build_dir.srcdir == src_dir:
1062                 return # We already did this.
1063             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(build_dir, build_dir.srcdir)
1064         build_dir.link(src_dir, duplicate)
1065
1066     def Repository(self, *dirs):
1067         """Specify Repository directories to search."""
1068         for d in dirs:
1069             if not isinstance(d, SCons.Node.Node):
1070                 d = self.Dir(d)
1071             self.Top.addRepository(d)
1072
1073     def Rfindalldirs(self, pathlist, cwd):
1074         """__cacheable__"""
1075         if SCons.Util.is_String(pathlist):
1076             pathlist = string.split(pathlist, os.pathsep)
1077         if not SCons.Util.is_List(pathlist):
1078             pathlist = [pathlist]
1079         result = []
1080         for path in filter(None, pathlist):
1081             if isinstance(path, SCons.Node.Node):
1082                 result.append(path)
1083                 continue
1084             path, dir = self._transformPath(path, cwd)
1085             dir = dir.Dir(path)
1086             result.extend(dir.get_all_rdirs())
1087         return result
1088
1089     def CacheDir(self, path):
1090         self.CachePath = path
1091
1092     def build_dir_target_climb(self, orig, dir, tail):
1093         """Create targets in corresponding build directories
1094
1095         Climb the directory tree, and look up path names
1096         relative to any linked build directories we find.
1097         __cacheable__
1098         """
1099         targets = []
1100         message = None
1101         fmt = "building associated BuildDir targets: %s"
1102         start_dir = dir
1103         while dir:
1104             for bd in dir.build_dirs:
1105                 if start_dir.is_under(bd):
1106                     # If already in the build-dir location, don't reflect
1107                     return [orig], fmt % str(orig)
1108                 p = apply(os.path.join, [bd.path] + tail)
1109                 targets.append(self.Entry(p))
1110             tail = [dir.name] + tail
1111             dir = dir.up()
1112         if targets:
1113             message = fmt % string.join(map(str, targets))
1114         return targets, message
1115
1116 class Dir(Base):
1117     """A class for directories in a file system.
1118     """
1119
1120     def __init__(self, name, directory, fs):
1121         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1122         Base.__init__(self, name, directory, fs)
1123         self._morph()
1124
1125     def _morph(self):
1126         """Turn a file system Node (either a freshly initialized directory
1127         object or a separate Entry object) into a proper directory object.
1128
1129         Set up this directory's entries and hook it into the file
1130         system tree.  Specify that directories (this Node) don't use
1131         signatures for calculating whether they're current.
1132         __cache_reset__"""
1133
1134         self.repositories = []
1135         self.srcdir = None
1136
1137         self.entries = {}
1138         self.entries['.'] = self
1139         self.entries['..'] = self.dir
1140         self.cwd = self
1141         self.searched = 0
1142         self._sconsign = None
1143         self.build_dirs = []
1144
1145         # Don't just reset the executor, replace its action list,
1146         # because it might have some pre-or post-actions that need to
1147         # be preserved.
1148         self.builder = get_MkdirBuilder()
1149         self.get_executor().set_action_list(self.builder.action)
1150
1151     def diskcheck_match(self):
1152         diskcheck_match(self, self.fs.isfile,
1153                            "File %s found where directory expected.")
1154
1155     def disambiguate(self):
1156         return self
1157
1158     def __clearRepositoryCache(self, duplicate=None):
1159         """Called when we change the repository(ies) for a directory.
1160         This clears any cached information that is invalidated by changing
1161         the repository."""
1162
1163         for node in self.entries.values():
1164             if node != self.dir:
1165                 if node != self and isinstance(node, Dir):
1166                     node.__clearRepositoryCache(duplicate)
1167                 else:
1168                     node.clear()
1169                     try:
1170                         del node._srcreps
1171                     except AttributeError:
1172                         pass
1173                     if duplicate != None:
1174                         node.duplicate=duplicate
1175     
1176     def __resetDuplicate(self, node):
1177         if node != self:
1178             node.duplicate = node.get_dir().duplicate
1179
1180     def Entry(self, name):
1181         """Create an entry node named 'name' relative to this directory."""
1182         return self.fs.Entry(name, self)
1183
1184     def Dir(self, name):
1185         """Create a directory node named 'name' relative to this directory."""
1186         return self.fs.Dir(name, self)
1187
1188     def File(self, name):
1189         """Create a file node named 'name' relative to this directory."""
1190         return self.fs.File(name, self)
1191
1192     def link(self, srcdir, duplicate):
1193         """Set this directory as the build directory for the
1194         supplied source directory."""
1195         self.srcdir = srcdir
1196         self.duplicate = duplicate
1197         self.__clearRepositoryCache(duplicate)
1198         srcdir.build_dirs.append(self)
1199
1200     def getRepositories(self):
1201         """Returns a list of repositories for this directory.
1202         __cacheable__"""
1203         if self.srcdir and not self.duplicate:
1204             return self.srcdir.get_all_rdirs() + self.repositories
1205         return self.repositories
1206
1207     def get_all_rdirs(self):
1208         """__cacheable__"""
1209         result = [self]
1210         fname = '.'
1211         dir = self
1212         while dir:
1213             for rep in dir.getRepositories():
1214                 result.append(rep.Dir(fname))
1215             fname = dir.name + os.sep + fname
1216             dir = dir.up()
1217         return result
1218
1219     def addRepository(self, dir):
1220         if dir != self and not dir in self.repositories:
1221             self.repositories.append(dir)
1222             dir.tpath = '.'
1223             self.__clearRepositoryCache()
1224
1225     def up(self):
1226         return self.entries['..']
1227
1228     def rel_path(self, other):
1229         """Return a path to "other" relative to this directory.
1230         __cacheable__"""
1231         if isinstance(other, Dir):
1232             name = []
1233         else:
1234             try:
1235                 name = [other.name]
1236                 other = other.dir
1237             except AttributeError:
1238                 return str(other)
1239         if self is other:
1240             return name and name[0] or '.'
1241         i = 0
1242         for x, y in map(None, self.path_elements, other.path_elements):
1243             if not x is y:
1244                 break
1245             i = i + 1
1246         path_elems = ['..']*(len(self.path_elements)-i) \
1247                    + map(lambda n: n.name, other.path_elements[i:]) \
1248                    + name
1249              
1250         return string.join(path_elems, os.sep)
1251
1252     def scan(self):
1253         if not self.implicit is None:
1254             return
1255         self.implicit = []
1256         self.implicit_dict = {}
1257         self._children_reset()
1258
1259         dont_scan = lambda k: k not in ['.', '..', '.sconsign']
1260         deps = filter(dont_scan, self.entries.keys())
1261         # keys() is going to give back the entries in an internal,
1262         # unsorted order.  Sort 'em so the order is deterministic.
1263         deps.sort()
1264         entries = map(lambda n, e=self.entries: e[n], deps)
1265
1266         self._add_child(self.implicit, self.implicit_dict, entries)
1267
1268     def get_found_includes(self, env, scanner, path):
1269         """Return the included implicit dependencies in this file.
1270         Cache results so we only scan the file once per path
1271         regardless of how many times this information is requested.
1272         __cacheable__"""
1273         if not scanner:
1274             return []
1275         # Clear cached info for this Node.  If we already visited this
1276         # directory on our walk down the tree (because we didn't know at
1277         # that point it was being used as the source for another Node)
1278         # then we may have calculated build signature before realizing
1279         # we had to scan the disk.  Now that we have to, though, we need
1280         # to invalidate the old calculated signature so that any node
1281         # dependent on our directory structure gets one that includes
1282         # info about everything on disk.
1283         self.clear()
1284         return scanner(self, env, path)
1285
1286     def build(self, **kw):
1287         """A null "builder" for directories."""
1288         global MkdirBuilder
1289         if not self.builder is MkdirBuilder:
1290             apply(SCons.Node.Node.build, [self,], kw)
1291
1292     def _create(self):
1293         """Create this directory, silently and without worrying about
1294         whether the builder is the default or not."""
1295         listDirs = []
1296         parent = self
1297         while parent:
1298             if parent.exists():
1299                 break
1300             listDirs.append(parent)
1301             p = parent.up()
1302             if p is None:
1303                 raise SCons.Errors.StopError, parent.path
1304             parent = p
1305         listDirs.reverse()
1306         for dirnode in listDirs:
1307             try:
1308                 # Don't call dirnode.build(), call the base Node method
1309                 # directly because we definitely *must* create this
1310                 # directory.  The dirnode.build() method will suppress
1311                 # the build if it's the default builder.
1312                 SCons.Node.Node.build(dirnode)
1313                 dirnode.get_executor().nullify()
1314                 # The build() action may or may not have actually
1315                 # created the directory, depending on whether the -n
1316                 # option was used or not.  Delete the _exists and
1317                 # _rexists attributes so they can be reevaluated.
1318                 dirnode.clear()
1319             except OSError:
1320                 pass
1321
1322     def multiple_side_effect_has_builder(self):
1323         global MkdirBuilder
1324         return not self.builder is MkdirBuilder and self.has_builder()
1325
1326     def alter_targets(self):
1327         """Return any corresponding targets in a build directory.
1328         """
1329         return self.fs.build_dir_target_climb(self, self, [])
1330
1331     def scanner_key(self):
1332         """A directory does not get scanned."""
1333         return None
1334
1335     def get_contents(self):
1336         """Return aggregate contents of all our children."""
1337         contents = cStringIO.StringIO()
1338         for kid in self.children():
1339             contents.write(kid.get_contents())
1340         return contents.getvalue()
1341
1342     def prepare(self):
1343         pass
1344
1345     def do_duplicate(self, src):
1346         pass
1347
1348     def current(self, calc=None):
1349         """If all of our children were up-to-date, then this
1350         directory was up-to-date, too."""
1351         if not self.builder is MkdirBuilder and not self.exists():
1352             return 0
1353         state = 0
1354         for kid in self.children():
1355             s = kid.get_state()
1356             if s and (not state or s > state):
1357                 state = s
1358         import SCons.Node
1359         if state == 0 or state == SCons.Node.up_to_date:
1360             return 1
1361         else:
1362             return 0
1363
1364     def rdir(self):
1365         "__cacheable__"
1366         if not self.exists():
1367             norm_name = _my_normcase(self.name)
1368             for dir in self.dir.get_all_rdirs():
1369                 try: node = dir.entries[norm_name]
1370                 except KeyError: node = dir.dir_on_disk(self.name)
1371                 if node and node.exists() and \
1372                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1373                         return node
1374         return self
1375
1376     def sconsign(self):
1377         """Return the .sconsign file info for this directory,
1378         creating it first if necessary."""
1379         if not self._sconsign:
1380             import SCons.SConsign
1381             self._sconsign = SCons.SConsign.ForDirectory(self)
1382         return self._sconsign
1383
1384     def srcnode(self):
1385         """Dir has a special need for srcnode()...if we
1386         have a srcdir attribute set, then that *is* our srcnode."""
1387         if self.srcdir:
1388             return self.srcdir
1389         return Base.srcnode(self)
1390
1391     def get_timestamp(self):
1392         """Return the latest timestamp from among our children"""
1393         stamp = 0
1394         for kid in self.children():
1395             if kid.get_timestamp() > stamp:
1396                 stamp = kid.get_timestamp()
1397         return stamp
1398
1399     def entry_abspath(self, name):
1400         return self.abspath + os.sep + name
1401
1402     def entry_path(self, name):
1403         return self.path + os.sep + name
1404
1405     def entry_tpath(self, name):
1406         return self.tpath + os.sep + name
1407
1408     def must_be_a_Dir(self):
1409         """Called to make sure a Node is a Dir.  Since we're already
1410         one, this is a no-op for us."""
1411         return self
1412
1413     def entry_exists_on_disk(self, name):
1414         """__cacheable__"""
1415         return self.fs.exists(self.entry_abspath(name))
1416
1417     def srcdir_list(self):
1418         """__cacheable__"""
1419         result = []
1420
1421         dirname = '.'
1422         dir = self
1423         while dir:
1424             if dir.srcdir:
1425                 d = dir.srcdir.Dir(dirname)
1426                 if d.is_under(dir):
1427                     # Shouldn't source from something in the build path:
1428                     # build_dir is probably under src_dir, in which case
1429                     # we are reflecting.
1430                     break
1431                 result.append(d)
1432             dirname = dir.name + os.sep + dirname
1433             dir = dir.up()
1434
1435         return result
1436
1437     def srcdir_duplicate(self, name):
1438         for dir in self.srcdir_list():
1439             if dir.entry_exists_on_disk(name):
1440                 srcnode = dir.File(name)
1441                 if self.duplicate:
1442                     node = self.File(name)
1443                     node.do_duplicate(srcnode)
1444                     return node
1445                 else:
1446                     return srcnode
1447         return None
1448
1449     def srcdir_find_file(self, filename):
1450         """__cacheable__"""
1451         def func(node):
1452             if (isinstance(node, File) or isinstance(node, Entry)) and \
1453                (node.is_derived() or node.is_pseudo_derived() or node.exists()):
1454                     return node
1455             return None
1456
1457         norm_name = _my_normcase(filename)
1458
1459         for rdir in self.get_all_rdirs():
1460             try: node = rdir.entries[norm_name]
1461             except KeyError: node = rdir.file_on_disk(filename)
1462             else: node = func(node)
1463             if node:
1464                 return node, self
1465
1466         for srcdir in self.srcdir_list():
1467             for rdir in srcdir.get_all_rdirs():
1468                 try: node = rdir.entries[norm_name]
1469                 except KeyError: node = rdir.file_on_disk(filename)
1470                 else: node = func(node)
1471                 if node:
1472                     return File(filename, self, self.fs), srcdir
1473
1474         return None, None
1475
1476     def dir_on_disk(self, name):
1477         if self.entry_exists_on_disk(name):
1478             try: return self.Dir(name)
1479             except TypeError: pass
1480         return None
1481
1482     def file_on_disk(self, name):
1483         if self.entry_exists_on_disk(name) or \
1484            diskcheck_rcs(self, name) or \
1485            diskcheck_sccs(self, name):
1486             try: return self.File(name)
1487             except TypeError: pass
1488         return self.srcdir_duplicate(name)
1489
1490 class RootDir(Dir):
1491     """A class for the root directory of a file system.
1492
1493     This is the same as a Dir class, except that the path separator
1494     ('/' or '\\') is actually part of the name, so we don't need to
1495     add a separator when creating the path names of entries within
1496     this directory.
1497     """
1498     def __init__(self, name, fs):
1499         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1500         # We're going to be our own parent directory (".." entry and .dir
1501         # attribute) so we have to set up some values so Base.__init__()
1502         # won't gag won't it calls some of our methods.
1503         self.abspath = ''
1504         self.path = ''
1505         self.tpath = ''
1506         self.path_elements = []
1507         self.duplicate = 0
1508         Base.__init__(self, name, self, fs)
1509
1510         # Now set our paths to what we really want them to be: the
1511         # initial drive letter (the name) plus the directory separator.
1512         self.abspath = name + os.sep
1513         self.path = name + os.sep
1514         self.tpath = name + os.sep
1515         self._morph()
1516
1517     def __str__(self):
1518         return self.abspath
1519
1520     def entry_abspath(self, name):
1521         return self.abspath + name
1522
1523     def entry_path(self, name):
1524         return self.path + name
1525
1526     def entry_tpath(self, name):
1527         return self.tpath + name
1528
1529     def is_under(self, dir):
1530         if self is dir:
1531             return 1
1532         else:
1533             return 0
1534
1535     def up(self):
1536         return None
1537
1538     def get_dir(self):
1539         return None
1540
1541     def src_builder(self):
1542         return _null
1543
1544 class NodeInfo(SCons.Node.NodeInfo):
1545     # The bsig attributes needs to stay here, if it's initialized in
1546     # __init__() then the assignment seems to overwrite any values
1547     # unpickled from .sconsign files.
1548     bsig = None
1549     def __cmp__(self, other):
1550         return cmp(self.bsig, other.bsig)
1551     def update(self, node):
1552         self.timestamp = node.get_timestamp()
1553         self.size = node.getsize()
1554
1555 class BuildInfo(SCons.Node.BuildInfo):
1556     def __init__(self, node):
1557         SCons.Node.BuildInfo.__init__(self, node)
1558         self.node = node
1559     def convert_to_sconsign(self):
1560         """Convert this BuildInfo object for writing to a .sconsign file
1561
1562         We hung onto the node that we refer to so that we can translate
1563         the lists of bsources, bdepends and bimplicit Nodes into strings
1564         relative to the node, but we don't want to write out that Node
1565         itself to the .sconsign file, so we delete the attribute in
1566         preparation.
1567         """
1568         rel_path = self.node.rel_path
1569         delattr(self, 'node')
1570         for attr in ['bsources', 'bdepends', 'bimplicit']:
1571             try:
1572                 val = getattr(self, attr)
1573             except AttributeError:
1574                 pass
1575             else:
1576                 setattr(self, attr, map(rel_path, val))
1577     def convert_from_sconsign(self, dir, name):
1578         """Convert a newly-read BuildInfo object for in-SCons use
1579
1580         An on-disk BuildInfo comes without a reference to the node
1581         for which it's intended, so we have to convert the arguments
1582         and add back a self.node attribute.  The bsources, bdepends and
1583         bimplicit lists all come from disk as paths relative to that node,
1584         so convert them to actual Nodes for use by the rest of SCons.
1585         """
1586         self.node = dir.Entry(name)
1587         Entry_func = self.node.dir.Entry
1588         for attr in ['bsources', 'bdepends', 'bimplicit']:
1589             try:
1590                 val = getattr(self, attr)
1591             except AttributeError:
1592                 pass
1593             else:
1594                 setattr(self, attr, map(Entry_func, val))
1595
1596 class File(Base):
1597     """A class for files in a file system.
1598     """
1599     def diskcheck_match(self):
1600         diskcheck_match(self, self.fs.isdir,
1601                            "Directory %s found where file expected.")
1602
1603     def __init__(self, name, directory, fs):
1604         if __debug__: logInstanceCreation(self, 'Node.FS.File')
1605         Base.__init__(self, name, directory, fs)
1606         self._morph()
1607
1608     def Entry(self, name):
1609         """Create an entry node named 'name' relative to
1610         the SConscript directory of this file."""
1611         return self.fs.Entry(name, self.cwd)
1612
1613     def Dir(self, name):
1614         """Create a directory node named 'name' relative to
1615         the SConscript directory of this file."""
1616         return self.fs.Dir(name, self.cwd)
1617
1618     def Dirs(self, pathlist):
1619         """Create a list of directories relative to the SConscript
1620         directory of this file."""
1621         return map(lambda p, s=self: s.Dir(p), pathlist)
1622
1623     def File(self, name):
1624         """Create a file node named 'name' relative to
1625         the SConscript directory of this file."""
1626         return self.fs.File(name, self.cwd)
1627
1628     def RDirs(self, pathlist):
1629         """Search for a list of directories in the Repository list."""
1630         return self.fs.Rfindalldirs(pathlist, self.cwd)
1631
1632     def _morph(self):
1633         """Turn a file system node into a File object.  __cache_reset__"""
1634         self.scanner_paths = {}
1635         if not hasattr(self, '_local'):
1636             self._local = 0
1637
1638     def disambiguate(self):
1639         return self
1640
1641     def scanner_key(self):
1642         return self.get_suffix()
1643
1644     def get_contents(self):
1645         if not self.rexists():
1646             return ''
1647         return open(self.rfile().abspath, "rb").read()
1648
1649     def get_timestamp(self):
1650         if self.rexists():
1651             return self.rfile().getmtime()
1652         else:
1653             return 0
1654
1655     def store_info(self, obj):
1656         # Merge our build information into the already-stored entry.
1657         # This accomodates "chained builds" where a file that's a target
1658         # in one build (SConstruct file) is a source in a different build.
1659         # See test/chained-build.py for the use case.
1660         entry = self.get_stored_info()
1661         entry.merge(obj)
1662         self.dir.sconsign().set_entry(self.name, entry)
1663
1664     def get_stored_info(self):
1665         "__cacheable__"
1666         try:
1667             stored = self.dir.sconsign().get_entry(self.name)
1668         except (KeyError, OSError):
1669             return self.new_binfo()
1670         else:
1671             if not hasattr(stored, 'ninfo'):
1672                 # Transition:  The .sconsign file entry has no NodeInfo
1673                 # object, which means it's a slightly older BuildInfo.
1674                 # Copy over the relevant attributes.
1675                 ninfo = stored.ninfo = self.new_ninfo()
1676                 for attr in ninfo.__dict__.keys():
1677                     try:
1678                         setattr(ninfo, attr, getattr(stored, attr))
1679                     except AttributeError:
1680                         pass
1681             return stored
1682
1683     def get_stored_implicit(self):
1684         binfo = self.get_stored_info()
1685         try: return binfo.bimplicit
1686         except AttributeError: return None
1687
1688     def rel_path(self, other):
1689         return self.dir.rel_path(other)
1690
1691     def get_found_includes(self, env, scanner, path):
1692         """Return the included implicit dependencies in this file.
1693         Cache results so we only scan the file once per path
1694         regardless of how many times this information is requested.
1695         __cacheable__"""
1696         if not scanner:
1697             return []
1698         return scanner(self, env, path)
1699
1700     def _createDir(self):
1701         # ensure that the directories for this node are
1702         # created.
1703         self.dir._create()
1704
1705     def retrieve_from_cache(self):
1706         """Try to retrieve the node's content from a cache
1707
1708         This method is called from multiple threads in a parallel build,
1709         so only do thread safe stuff here. Do thread unsafe stuff in
1710         built().
1711
1712         Note that there's a special trick here with the execute flag
1713         (one that's not normally done for other actions).  Basically
1714         if the user requested a noexec (-n) build, then
1715         SCons.Action.execute_actions is set to 0 and when any action
1716         is called, it does its showing but then just returns zero
1717         instead of actually calling the action execution operation.
1718         The problem for caching is that if the file does NOT exist in
1719         cache then the CacheRetrieveString won't return anything to
1720         show for the task, but the Action.__call__ won't call
1721         CacheRetrieveFunc; instead it just returns zero, which makes
1722         the code below think that the file *was* successfully
1723         retrieved from the cache, therefore it doesn't do any
1724         subsequent building.  However, the CacheRetrieveString didn't
1725         print anything because it didn't actually exist in the cache,
1726         and no more build actions will be performed, so the user just
1727         sees nothing.  The fix is to tell Action.__call__ to always
1728         execute the CacheRetrieveFunc and then have the latter
1729         explicitly check SCons.Action.execute_actions itself.
1730
1731         Returns true iff the node was successfully retrieved.
1732         """
1733         b = self.is_derived()
1734         if not b and not self.has_src_builder():
1735             return None
1736         if b and self.fs.CachePath:
1737             if self.fs.cache_show:
1738                 if CacheRetrieveSilent(self, [], None, execute=1) == 0:
1739                     self.build(presub=0, execute=0)
1740                     return 1
1741             elif CacheRetrieve(self, [], None, execute=1) == 0:
1742                 return 1
1743         return None
1744
1745     def built(self):
1746         """Called just after this node is successfully built.
1747         __cache_reset__"""
1748         # Push this file out to cache before the superclass Node.built()
1749         # method has a chance to clear the build signature, which it
1750         # will do if this file has a source scanner.
1751         if self.fs.CachePath and self.exists():
1752             CachePush(self, [], None)
1753         self.fs.clear_cache()
1754         SCons.Node.Node.built(self)
1755
1756     def visited(self):
1757         if self.fs.CachePath and self.fs.cache_force and self.exists():
1758             CachePush(self, None, None)
1759
1760     def has_src_builder(self):
1761         """Return whether this Node has a source builder or not.
1762
1763         If this Node doesn't have an explicit source code builder, this
1764         is where we figure out, on the fly, if there's a transparent
1765         source code builder for it.
1766
1767         Note that if we found a source builder, we also set the
1768         self.builder attribute, so that all of the methods that actually
1769         *build* this file don't have to do anything different.
1770         """
1771         try:
1772             scb = self.sbuilder
1773         except AttributeError:
1774             if self.rexists():
1775                 scb = None
1776             else:
1777                 scb = self.dir.src_builder()
1778                 if scb is _null:
1779                     if diskcheck_sccs(self.dir, self.name):
1780                         scb = get_DefaultSCCSBuilder()
1781                     elif diskcheck_rcs(self.dir, self.name):
1782                         scb = get_DefaultRCSBuilder()
1783                     else:
1784                         scb = None
1785                 if scb is not None:
1786                     self.builder_set(scb)
1787             self.sbuilder = scb
1788         return not scb is None
1789
1790     def alter_targets(self):
1791         """Return any corresponding targets in a build directory.
1792         """
1793         if self.is_derived():
1794             return [], None
1795         return self.fs.build_dir_target_climb(self, self.dir, [self.name])
1796
1797     def is_pseudo_derived(self):
1798         "__cacheable__"
1799         return self.has_src_builder()
1800
1801     def _rmv_existing(self):
1802         '__cache_reset__'
1803         Unlink(self, [], None)
1804         
1805     def prepare(self):
1806         """Prepare for this file to be created."""
1807         SCons.Node.Node.prepare(self)
1808
1809         if self.get_state() != SCons.Node.up_to_date:
1810             if self.exists():
1811                 if self.is_derived() and not self.precious:
1812                     self._rmv_existing()
1813             else:
1814                 try:
1815                     self._createDir()
1816                 except SCons.Errors.StopError, drive:
1817                     desc = "No drive `%s' for target `%s'." % (drive, self)
1818                     raise SCons.Errors.StopError, desc
1819
1820     def remove(self):
1821         """Remove this file."""
1822         if self.exists() or self.islink():
1823             self.fs.unlink(self.path)
1824             return 1
1825         return None
1826
1827     def do_duplicate(self, src):
1828         self._createDir()
1829         try:
1830             Unlink(self, None, None)
1831         except SCons.Errors.BuildError:
1832             pass
1833         try:
1834             Link(self, src, None)
1835         except SCons.Errors.BuildError, e:
1836             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
1837             raise SCons.Errors.StopError, desc
1838         self.linked = 1
1839         # The Link() action may or may not have actually
1840         # created the file, depending on whether the -n
1841         # option was used or not.  Delete the _exists and
1842         # _rexists attributes so they can be reevaluated.
1843         self.clear()
1844
1845     def exists(self):
1846         "__cacheable__"
1847         # Duplicate from source path if we are set up to do this.
1848         if self.duplicate and not self.is_derived() and not self.linked:
1849             src=self.srcnode()
1850             if src is self:
1851                 return Base.exists(self)
1852             src = src.rfile()
1853             if src.abspath != self.abspath and src.exists():
1854                 self.do_duplicate(src)
1855         return Base.exists(self)
1856
1857     #
1858     # SIGNATURE SUBSYSTEM
1859     #
1860
1861     def new_binfo(self):
1862         return BuildInfo(self)
1863
1864     def new_ninfo(self):
1865         ninfo = NodeInfo()
1866         ninfo.update(self)
1867         return ninfo
1868
1869     def get_csig(self, calc=None):
1870         """
1871         Generate a node's content signature, the digested signature
1872         of its content.
1873
1874         node - the node
1875         cache - alternate node to use for the signature cache
1876         returns - the content signature
1877         """
1878         try:
1879             return self.binfo.ninfo.csig
1880         except AttributeError:
1881             pass
1882
1883         if calc is None:
1884             calc = self.calculator()
1885
1886         max_drift = self.fs.max_drift
1887         mtime = self.get_timestamp()
1888         use_stored = max_drift >= 0 and (time.time() - mtime) > max_drift
1889
1890         csig = None
1891         if use_stored:
1892             old = self.get_stored_info().ninfo
1893             try:
1894                 if old.timestamp and old.csig and old.timestamp == mtime:
1895                     csig = old.csig
1896             except AttributeError:
1897                 pass
1898         if csig is None:
1899             csig = calc.module.signature(self)
1900
1901         binfo = self.get_binfo()
1902         ninfo = binfo.ninfo
1903         ninfo.csig = csig
1904         ninfo.update(self)
1905
1906         if use_stored:
1907             self.store_info(binfo)
1908
1909         return csig
1910
1911     #
1912     #
1913     #
1914
1915     def current(self, calc=None):
1916         self.binfo = self.gen_binfo(calc)
1917         return self._cur2()
1918     def _cur2(self):
1919         "__cacheable__"
1920         if self.always_build:
1921             return None
1922         if not self.exists():
1923             # The file doesn't exist locally...
1924             r = self.rfile()
1925             if r != self:
1926                 # ...but there is one in a Repository...
1927                 old = r.get_stored_info()
1928                 new = self.get_binfo()
1929                 if new == old:
1930                     # ...and it's even up-to-date...
1931                     if self._local:
1932                         # ...and they'd like a local copy.
1933                         LocalCopy(self, r, None)
1934                         self.store_info(new)
1935                     return 1
1936             return None
1937         else:
1938             old = self.get_stored_info()
1939             new = self.get_binfo()
1940             return (new == old)
1941
1942     def rfile(self):
1943         "__cacheable__"
1944         if not self.exists():
1945             norm_name = _my_normcase(self.name)
1946             for dir in self.dir.get_all_rdirs():
1947                 try: node = dir.entries[norm_name]
1948                 except KeyError: node = dir.file_on_disk(self.name)
1949                 if node and node.exists() and \
1950                    (isinstance(node, File) or isinstance(node, Entry) \
1951                     or not node.is_derived()):
1952                         return node
1953         return self
1954
1955     def rstr(self):
1956         return str(self.rfile())
1957
1958     def cachepath(self):
1959         if not self.fs.CachePath:
1960             return None, None
1961         ninfo = self.get_binfo().ninfo
1962         if not hasattr(ninfo, 'bsig'):
1963             raise SCons.Errors.InternalError, "cachepath(%s) found no bsig" % self.path
1964         elif ninfo.bsig is None:
1965             raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
1966         # Add the path to the cache signature, because multiple
1967         # targets built by the same action will all have the same
1968         # build signature, and we have to differentiate them somehow.
1969         cache_sig = SCons.Sig.MD5.collect([ninfo.bsig, self.path])
1970         subdir = string.upper(cache_sig[0])
1971         dir = os.path.join(self.fs.CachePath, subdir)
1972         return dir, os.path.join(dir, cache_sig)
1973
1974     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
1975         return self.dir.File(prefix + splitext(self.name)[0] + suffix)
1976
1977     def must_be_a_Dir(self):
1978         """Called to make sure a Node is a Dir.  Since we're already a
1979         File, this is a TypeError..."""
1980         raise TypeError, "Tried to lookup File '%s' as a Dir." % self.path
1981
1982 default_fs = None
1983
1984 def find_file(filename, paths, verbose=None):
1985     """
1986     find_file(str, [Dir()]) -> [nodes]
1987
1988     filename - a filename to find
1989     paths - a list of directory path *nodes* to search in.  Can be
1990             represented as a list, a tuple, or a callable that is
1991             called with no arguments and returns the list or tuple.
1992
1993     returns - the node created from the found file.
1994
1995     Find a node corresponding to either a derived file or a file
1996     that exists already.
1997
1998     Only the first file found is returned, and none is returned
1999     if no file is found.
2000     __cacheable__
2001     """
2002     if verbose:
2003         if not SCons.Util.is_String(verbose):
2004             verbose = "find_file"
2005         if not callable(verbose):
2006             verbose = '  %s: ' % verbose
2007             verbose = lambda s, v=verbose: sys.stdout.write(v + s)
2008     else:
2009         verbose = lambda x: x
2010
2011     if callable(paths):
2012         paths = paths()
2013
2014     # Give Entries a chance to morph into Dirs.
2015     paths = map(lambda p: p.must_be_a_Dir(), paths)
2016
2017     filedir, filename = os.path.split(filename)
2018     if filedir:
2019         def filedir_lookup(p, fd=filedir):
2020             try:
2021                 return p.Dir(fd)
2022             except TypeError:
2023                 # We tried to look up a Dir, but it seems there's already
2024                 # a File (or something else) there.  No big.
2025                 return None
2026         paths = filter(None, map(filedir_lookup, paths))
2027
2028     for dir in paths:
2029         verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
2030         node, d = dir.srcdir_find_file(filename)
2031         if node:
2032             verbose("... FOUND '%s' in '%s'\n" % (filename, d))
2033             return node
2034     return None
2035
2036 def find_files(filenames, paths):
2037     """
2038     find_files([str], [Dir()]) -> [nodes]
2039
2040     filenames - a list of filenames to find
2041     paths - a list of directory path *nodes* to search in
2042
2043     returns - the nodes created from the found files.
2044
2045     Finds nodes corresponding to either derived files or files
2046     that exist already.
2047
2048     Only the first file found is returned for each filename,
2049     and any files that aren't found are ignored.
2050     """
2051     nodes = map(lambda x, paths=paths: find_file(x, paths), filenames)
2052     return filter(None, nodes)