Issue 1086: add support for generic batch build actions, and
[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 from itertools import izip
39 import cStringIO
40 import fnmatch
41 import os
42 import os.path
43 import re
44 import shutil
45 import stat
46 import string
47 import sys
48 import time
49
50 try:
51     import codecs
52 except ImportError:
53     pass
54 else:
55     # TODO(2.2):  Remove when 2.3 becomes the minimal supported version.
56     try:
57         codecs.BOM_UTF8
58     except AttributeError:
59         codecs.BOM_UTF8 = '\xef\xbb\xbf'
60     try:
61         codecs.BOM_UTF16
62     except AttributeError:
63         if sys.byteorder == 'little':
64             codecs.BOM_UTF16 = '\xff\xfe'
65         else:
66             codecs.BOM_UTF16 = '\xfe\xff'
67
68 import SCons.Action
69 from SCons.Debug import logInstanceCreation
70 import SCons.Errors
71 import SCons.Memoize
72 import SCons.Node
73 import SCons.Node.Alias
74 import SCons.Subst
75 import SCons.Util
76 import SCons.Warnings
77
78 from SCons.Debug import Trace
79
80 do_store_info = True
81
82
83 class EntryProxyAttributeError(AttributeError):
84     """
85     An AttributeError subclass for recording and displaying the name
86     of the underlying Entry involved in an AttributeError exception.
87     """
88     def __init__(self, entry_proxy, attribute):
89         AttributeError.__init__(self)
90         self.entry_proxy = entry_proxy
91         self.attribute = attribute
92     def __str__(self):
93         entry = self.entry_proxy.get()
94         fmt = "%s instance %s has no attribute %s"
95         return fmt % (entry.__class__.__name__,
96                       repr(entry.name),
97                       repr(self.attribute))
98
99 # The max_drift value:  by default, use a cached signature value for
100 # any file that's been untouched for more than two days.
101 default_max_drift = 2*24*60*60
102
103 #
104 # We stringify these file system Nodes a lot.  Turning a file system Node
105 # into a string is non-trivial, because the final string representation
106 # can depend on a lot of factors:  whether it's a derived target or not,
107 # whether it's linked to a repository or source directory, and whether
108 # there's duplication going on.  The normal technique for optimizing
109 # calculations like this is to memoize (cache) the string value, so you
110 # only have to do the calculation once.
111 #
112 # A number of the above factors, however, can be set after we've already
113 # been asked to return a string for a Node, because a Repository() or
114 # VariantDir() call or the like may not occur until later in SConscript
115 # files.  So this variable controls whether we bother trying to save
116 # string values for Nodes.  The wrapper interface can set this whenever
117 # they're done mucking with Repository and VariantDir and the other stuff,
118 # to let this module know it can start returning saved string values
119 # for Nodes.
120 #
121 Save_Strings = None
122
123 def save_strings(val):
124     global Save_Strings
125     Save_Strings = val
126
127 #
128 # Avoid unnecessary function calls by recording a Boolean value that
129 # tells us whether or not os.path.splitdrive() actually does anything
130 # on this system, and therefore whether we need to bother calling it
131 # when looking up path names in various methods below.
132
133
134 do_splitdrive = None
135
136 def initialize_do_splitdrive():
137     global do_splitdrive
138     drive, path = os.path.splitdrive('X:/foo')
139     do_splitdrive = not not drive
140
141 initialize_do_splitdrive()
142
143 #
144
145 needs_normpath_check = None
146
147 def initialize_normpath_check():
148     """
149     Initialize the normpath_check regular expression.
150
151     This function is used by the unit tests to re-initialize the pattern
152     when testing for behavior with different values of os.sep.
153     """
154     global needs_normpath_check
155     if os.sep == '/':
156         pattern = r'.*/|\.$|\.\.$'
157     else:
158         pattern = r'.*[/%s]|\.$|\.\.$' % re.escape(os.sep)
159     needs_normpath_check = re.compile(pattern)
160
161 initialize_normpath_check()
162
163 #
164 # SCons.Action objects for interacting with the outside world.
165 #
166 # The Node.FS methods in this module should use these actions to
167 # create and/or remove files and directories; they should *not* use
168 # os.{link,symlink,unlink,mkdir}(), etc., directly.
169 #
170 # Using these SCons.Action objects ensures that descriptions of these
171 # external activities are properly displayed, that the displays are
172 # suppressed when the -s (silent) option is used, and (most importantly)
173 # the actions are disabled when the the -n option is used, in which case
174 # there should be *no* changes to the external file system(s)...
175 #
176
177 if hasattr(os, 'link'):
178     def _hardlink_func(fs, src, dst):
179         # If the source is a symlink, we can't just hard-link to it
180         # because a relative symlink may point somewhere completely
181         # different.  We must disambiguate the symlink and then
182         # hard-link the final destination file.
183         while fs.islink(src):
184             link = fs.readlink(src)
185             if not os.path.isabs(link):
186                 src = link
187             else:
188                 src = os.path.join(os.path.dirname(src), link)
189         fs.link(src, dst)
190 else:
191     _hardlink_func = None
192
193 if hasattr(os, 'symlink'):
194     def _softlink_func(fs, src, dst):
195         fs.symlink(src, dst)
196 else:
197     _softlink_func = None
198
199 def _copy_func(fs, src, dest):
200     shutil.copy2(src, dest)
201     st = fs.stat(src)
202     fs.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
203
204
205 Valid_Duplicates = ['hard-soft-copy', 'soft-hard-copy',
206                     'hard-copy', 'soft-copy', 'copy']
207
208 Link_Funcs = [] # contains the callables of the specified duplication style
209
210 def set_duplicate(duplicate):
211     # Fill in the Link_Funcs list according to the argument
212     # (discarding those not available on the platform).
213
214     # Set up the dictionary that maps the argument names to the
215     # underlying implementations.  We do this inside this function,
216     # not in the top-level module code, so that we can remap os.link
217     # and os.symlink for testing purposes.
218     link_dict = {
219         'hard' : _hardlink_func,
220         'soft' : _softlink_func,
221         'copy' : _copy_func
222     }
223
224     if not duplicate in Valid_Duplicates:
225         raise SCons.Errors.InternalError, ("The argument of set_duplicate "
226                                            "should be in Valid_Duplicates")
227     global Link_Funcs
228     Link_Funcs = []
229     for func in string.split(duplicate,'-'):
230         if link_dict[func]:
231             Link_Funcs.append(link_dict[func])
232
233 def LinkFunc(target, source, env):
234     # Relative paths cause problems with symbolic links, so
235     # we use absolute paths, which may be a problem for people
236     # who want to move their soft-linked src-trees around. Those
237     # people should use the 'hard-copy' mode, softlinks cannot be
238     # used for that; at least I have no idea how ...
239     src = source[0].abspath
240     dest = target[0].abspath
241     dir, file = os.path.split(dest)
242     if dir and not target[0].fs.isdir(dir):
243         os.makedirs(dir)
244     if not Link_Funcs:
245         # Set a default order of link functions.
246         set_duplicate('hard-soft-copy')
247     fs = source[0].fs
248     # Now link the files with the previously specified order.
249     for func in Link_Funcs:
250         try:
251             func(fs, src, dest)
252             break
253         except (IOError, OSError):
254             # An OSError indicates something happened like a permissions
255             # problem or an attempt to symlink across file-system
256             # boundaries.  An IOError indicates something like the file
257             # not existing.  In either case, keeping trying additional
258             # functions in the list and only raise an error if the last
259             # one failed.
260             if func == Link_Funcs[-1]:
261                 # exception of the last link method (copy) are fatal
262                 raise
263     return 0
264
265 Link = SCons.Action.Action(LinkFunc, None)
266 def LocalString(target, source, env):
267     return 'Local copy of %s from %s' % (target[0], source[0])
268
269 LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
270
271 def UnlinkFunc(target, source, env):
272     t = target[0]
273     t.fs.unlink(t.abspath)
274     return 0
275
276 Unlink = SCons.Action.Action(UnlinkFunc, None)
277
278 def MkdirFunc(target, source, env):
279     t = target[0]
280     if not t.exists():
281         t.fs.mkdir(t.abspath)
282     return 0
283
284 Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
285
286 MkdirBuilder = None
287
288 def get_MkdirBuilder():
289     global MkdirBuilder
290     if MkdirBuilder is None:
291         import SCons.Builder
292         import SCons.Defaults
293         # "env" will get filled in by Executor.get_build_env()
294         # calling SCons.Defaults.DefaultEnvironment() when necessary.
295         MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
296                                              env = None,
297                                              explain = None,
298                                              is_explicit = None,
299                                              target_scanner = SCons.Defaults.DirEntryScanner,
300                                              name = "MkdirBuilder")
301     return MkdirBuilder
302
303 class _Null:
304     pass
305
306 _null = _Null()
307
308 DefaultSCCSBuilder = None
309 DefaultRCSBuilder = None
310
311 def get_DefaultSCCSBuilder():
312     global DefaultSCCSBuilder
313     if DefaultSCCSBuilder is None:
314         import SCons.Builder
315         # "env" will get filled in by Executor.get_build_env()
316         # calling SCons.Defaults.DefaultEnvironment() when necessary.
317         act = SCons.Action.Action('$SCCSCOM', '$SCCSCOMSTR')
318         DefaultSCCSBuilder = SCons.Builder.Builder(action = act,
319                                                    env = None,
320                                                    name = "DefaultSCCSBuilder")
321     return DefaultSCCSBuilder
322
323 def get_DefaultRCSBuilder():
324     global DefaultRCSBuilder
325     if DefaultRCSBuilder is None:
326         import SCons.Builder
327         # "env" will get filled in by Executor.get_build_env()
328         # calling SCons.Defaults.DefaultEnvironment() when necessary.
329         act = SCons.Action.Action('$RCS_COCOM', '$RCS_COCOMSTR')
330         DefaultRCSBuilder = SCons.Builder.Builder(action = act,
331                                                   env = None,
332                                                   name = "DefaultRCSBuilder")
333     return DefaultRCSBuilder
334
335 # Cygwin's os.path.normcase pretends it's on a case-sensitive filesystem.
336 _is_cygwin = sys.platform == "cygwin"
337 if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
338     def _my_normcase(x):
339         return x
340 else:
341     def _my_normcase(x):
342         return string.upper(x)
343
344
345
346 class DiskChecker:
347     def __init__(self, type, do, ignore):
348         self.type = type
349         self.do = do
350         self.ignore = ignore
351         self.set_do()
352     def set_do(self):
353         self.__call__ = self.do
354     def set_ignore(self):
355         self.__call__ = self.ignore
356     def set(self, list):
357         if self.type in list:
358             self.set_do()
359         else:
360             self.set_ignore()
361
362 def do_diskcheck_match(node, predicate, errorfmt):
363     result = predicate()
364     try:
365         # If calling the predicate() cached a None value from stat(),
366         # remove it so it doesn't interfere with later attempts to
367         # build this Node as we walk the DAG.  (This isn't a great way
368         # to do this, we're reaching into an interface that doesn't
369         # really belong to us, but it's all about performance, so
370         # for now we'll just document the dependency...)
371         if node._memo['stat'] is None:
372             del node._memo['stat']
373     except (AttributeError, KeyError):
374         pass
375     if result:
376         raise TypeError, errorfmt % node.abspath
377
378 def ignore_diskcheck_match(node, predicate, errorfmt):
379     pass
380
381 def do_diskcheck_rcs(node, name):
382     try:
383         rcs_dir = node.rcs_dir
384     except AttributeError:
385         if node.entry_exists_on_disk('RCS'):
386             rcs_dir = node.Dir('RCS')
387         else:
388             rcs_dir = None
389         node.rcs_dir = rcs_dir
390     if rcs_dir:
391         return rcs_dir.entry_exists_on_disk(name+',v')
392     return None
393
394 def ignore_diskcheck_rcs(node, name):
395     return None
396
397 def do_diskcheck_sccs(node, name):
398     try:
399         sccs_dir = node.sccs_dir
400     except AttributeError:
401         if node.entry_exists_on_disk('SCCS'):
402             sccs_dir = node.Dir('SCCS')
403         else:
404             sccs_dir = None
405         node.sccs_dir = sccs_dir
406     if sccs_dir:
407         return sccs_dir.entry_exists_on_disk('s.'+name)
408     return None
409
410 def ignore_diskcheck_sccs(node, name):
411     return None
412
413 diskcheck_match = DiskChecker('match', do_diskcheck_match, ignore_diskcheck_match)
414 diskcheck_rcs = DiskChecker('rcs', do_diskcheck_rcs, ignore_diskcheck_rcs)
415 diskcheck_sccs = DiskChecker('sccs', do_diskcheck_sccs, ignore_diskcheck_sccs)
416
417 diskcheckers = [
418     diskcheck_match,
419     diskcheck_rcs,
420     diskcheck_sccs,
421 ]
422
423 def set_diskcheck(list):
424     for dc in diskcheckers:
425         dc.set(list)
426
427 def diskcheck_types():
428     return map(lambda dc: dc.type, diskcheckers)
429
430
431
432 class EntryProxy(SCons.Util.Proxy):
433     def __get_abspath(self):
434         entry = self.get()
435         return SCons.Subst.SpecialAttrWrapper(entry.get_abspath(),
436                                              entry.name + "_abspath")
437
438     def __get_filebase(self):
439         name = self.get().name
440         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[0],
441                                              name + "_filebase")
442
443     def __get_suffix(self):
444         name = self.get().name
445         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(name)[1],
446                                              name + "_suffix")
447
448     def __get_file(self):
449         name = self.get().name
450         return SCons.Subst.SpecialAttrWrapper(name, name + "_file")
451
452     def __get_base_path(self):
453         """Return the file's directory and file name, with the
454         suffix stripped."""
455         entry = self.get()
456         return SCons.Subst.SpecialAttrWrapper(SCons.Util.splitext(entry.get_path())[0],
457                                              entry.name + "_base")
458
459     def __get_posix_path(self):
460         """Return the path with / as the path separator,
461         regardless of platform."""
462         if os.sep == '/':
463             return self
464         else:
465             entry = self.get()
466             r = string.replace(entry.get_path(), os.sep, '/')
467             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
468
469     def __get_windows_path(self):
470         """Return the path with \ as the path separator,
471         regardless of platform."""
472         if os.sep == '\\':
473             return self
474         else:
475             entry = self.get()
476             r = string.replace(entry.get_path(), os.sep, '\\')
477             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
478
479     def __get_srcnode(self):
480         return EntryProxy(self.get().srcnode())
481
482     def __get_srcdir(self):
483         """Returns the directory containing the source node linked to this
484         node via VariantDir(), or the directory of this node if not linked."""
485         return EntryProxy(self.get().srcnode().dir)
486
487     def __get_rsrcnode(self):
488         return EntryProxy(self.get().srcnode().rfile())
489
490     def __get_rsrcdir(self):
491         """Returns the directory containing the source node linked to this
492         node via VariantDir(), or the directory of this node if not linked."""
493         return EntryProxy(self.get().srcnode().rfile().dir)
494
495     def __get_dir(self):
496         return EntryProxy(self.get().dir)
497
498     dictSpecialAttrs = { "base"     : __get_base_path,
499                          "posix"    : __get_posix_path,
500                          "windows"  : __get_windows_path,
501                          "win32"    : __get_windows_path,
502                          "srcpath"  : __get_srcnode,
503                          "srcdir"   : __get_srcdir,
504                          "dir"      : __get_dir,
505                          "abspath"  : __get_abspath,
506                          "filebase" : __get_filebase,
507                          "suffix"   : __get_suffix,
508                          "file"     : __get_file,
509                          "rsrcpath" : __get_rsrcnode,
510                          "rsrcdir"  : __get_rsrcdir,
511                        }
512
513     def __getattr__(self, name):
514         # This is how we implement the "special" attributes
515         # such as base, posix, srcdir, etc.
516         try:
517             attr_function = self.dictSpecialAttrs[name]
518         except KeyError:
519             try:
520                 attr = SCons.Util.Proxy.__getattr__(self, name)
521             except AttributeError, e:
522                 # Raise our own AttributeError subclass with an
523                 # overridden __str__() method that identifies the
524                 # name of the entry that caused the exception.
525                 raise EntryProxyAttributeError(self, name)
526             return attr
527         else:
528             return attr_function(self)
529
530 class Base(SCons.Node.Node):
531     """A generic class for file system entries.  This class is for
532     when we don't know yet whether the entry being looked up is a file
533     or a directory.  Instances of this class can morph into either
534     Dir or File objects by a later, more precise lookup.
535
536     Note: this class does not define __cmp__ and __hash__ for
537     efficiency reasons.  SCons does a lot of comparing of
538     Node.FS.{Base,Entry,File,Dir} objects, so those operations must be
539     as fast as possible, which means we want to use Python's built-in
540     object identity comparisons.
541     """
542
543     memoizer_counters = []
544
545     def __init__(self, name, directory, fs):
546         """Initialize a generic Node.FS.Base object.
547
548         Call the superclass initialization, take care of setting up
549         our relative and absolute paths, identify our parent
550         directory, and indicate that this node should use
551         signatures."""
552         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
553         SCons.Node.Node.__init__(self)
554
555         self.name = name
556         self.suffix = SCons.Util.splitext(name)[1]
557         self.fs = fs
558
559         assert directory, "A directory must be provided"
560
561         self.abspath = directory.entry_abspath(name)
562         self.labspath = directory.entry_labspath(name)
563         if directory.path == '.':
564             self.path = name
565         else:
566             self.path = directory.entry_path(name)
567         if directory.tpath == '.':
568             self.tpath = name
569         else:
570             self.tpath = directory.entry_tpath(name)
571         self.path_elements = directory.path_elements + [self]
572
573         self.dir = directory
574         self.cwd = None # will hold the SConscript directory for target nodes
575         self.duplicate = directory.duplicate
576
577     def str_for_display(self):
578         return '"' + self.__str__() + '"'
579
580     def must_be_same(self, klass):
581         """
582         This node, which already existed, is being looked up as the
583         specified klass.  Raise an exception if it isn't.
584         """
585         if isinstance(self, klass) or klass is Entry:
586             return
587         raise TypeError, "Tried to lookup %s '%s' as a %s." %\
588               (self.__class__.__name__, self.path, klass.__name__)
589
590     def get_dir(self):
591         return self.dir
592
593     def get_suffix(self):
594         return self.suffix
595
596     def rfile(self):
597         return self
598
599     def __str__(self):
600         """A Node.FS.Base object's string representation is its path
601         name."""
602         global Save_Strings
603         if Save_Strings:
604             return self._save_str()
605         return self._get_str()
606
607     memoizer_counters.append(SCons.Memoize.CountValue('_save_str'))
608
609     def _save_str(self):
610         try:
611             return self._memo['_save_str']
612         except KeyError:
613             pass
614         result = self._get_str()
615         self._memo['_save_str'] = result
616         return result
617
618     def _get_str(self):
619         global Save_Strings
620         if self.duplicate or self.is_derived():
621             return self.get_path()
622         srcnode = self.srcnode()
623         if srcnode.stat() is None and self.stat() is not None:
624             result = self.get_path()
625         else:
626             result = srcnode.get_path()
627         if not Save_Strings:
628             # We're not at the point where we're saving the string string
629             # representations of FS Nodes (because we haven't finished
630             # reading the SConscript files and need to have str() return
631             # things relative to them).  That also means we can't yet
632             # cache values returned (or not returned) by stat(), since
633             # Python code in the SConscript files might still create
634             # or otherwise affect the on-disk file.  So get rid of the
635             # values that the underlying stat() method saved.
636             try: del self._memo['stat']
637             except KeyError: pass
638             if self is not srcnode:
639                 try: del srcnode._memo['stat']
640                 except KeyError: pass
641         return result
642
643     rstr = __str__
644
645     memoizer_counters.append(SCons.Memoize.CountValue('stat'))
646
647     def stat(self):
648         try: return self._memo['stat']
649         except KeyError: pass
650         try: result = self.fs.stat(self.abspath)
651         except os.error: result = None
652         self._memo['stat'] = result
653         return result
654
655     def exists(self):
656         return self.stat() is not None
657
658     def rexists(self):
659         return self.rfile().exists()
660
661     def getmtime(self):
662         st = self.stat()
663         if st: return st[stat.ST_MTIME]
664         else: return None
665
666     def getsize(self):
667         st = self.stat()
668         if st: return st[stat.ST_SIZE]
669         else: return None
670
671     def isdir(self):
672         st = self.stat()
673         return st is not None and stat.S_ISDIR(st[stat.ST_MODE])
674
675     def isfile(self):
676         st = self.stat()
677         return st is not None and stat.S_ISREG(st[stat.ST_MODE])
678
679     if hasattr(os, 'symlink'):
680         def islink(self):
681             try: st = self.fs.lstat(self.abspath)
682             except os.error: return 0
683             return stat.S_ISLNK(st[stat.ST_MODE])
684     else:
685         def islink(self):
686             return 0                    # no symlinks
687
688     def is_under(self, dir):
689         if self is dir:
690             return 1
691         else:
692             return self.dir.is_under(dir)
693
694     def set_local(self):
695         self._local = 1
696
697     def srcnode(self):
698         """If this node is in a build path, return the node
699         corresponding to its source file.  Otherwise, return
700         ourself.
701         """
702         srcdir_list = self.dir.srcdir_list()
703         if srcdir_list:
704             srcnode = srcdir_list[0].Entry(self.name)
705             srcnode.must_be_same(self.__class__)
706             return srcnode
707         return self
708
709     def get_path(self, dir=None):
710         """Return path relative to the current working directory of the
711         Node.FS.Base object that owns us."""
712         if not dir:
713             dir = self.fs.getcwd()
714         if self == dir:
715             return '.'
716         path_elems = self.path_elements
717         try: i = path_elems.index(dir)
718         except ValueError: pass
719         else: path_elems = path_elems[i+1:]
720         path_elems = map(lambda n: n.name, path_elems)
721         return string.join(path_elems, os.sep)
722
723     def set_src_builder(self, builder):
724         """Set the source code builder for this node."""
725         self.sbuilder = builder
726         if not self.has_builder():
727             self.builder_set(builder)
728
729     def src_builder(self):
730         """Fetch the source code builder for this node.
731
732         If there isn't one, we cache the source code builder specified
733         for the directory (which in turn will cache the value from its
734         parent directory, and so on up to the file system root).
735         """
736         try:
737             scb = self.sbuilder
738         except AttributeError:
739             scb = self.dir.src_builder()
740             self.sbuilder = scb
741         return scb
742
743     def get_abspath(self):
744         """Get the absolute path of the file."""
745         return self.abspath
746
747     def for_signature(self):
748         # Return just our name.  Even an absolute path would not work,
749         # because that can change thanks to symlinks or remapped network
750         # paths.
751         return self.name
752
753     def get_subst_proxy(self):
754         try:
755             return self._proxy
756         except AttributeError:
757             ret = EntryProxy(self)
758             self._proxy = ret
759             return ret
760
761     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
762         """
763
764         Generates a target entry that corresponds to this entry (usually
765         a source file) with the specified prefix and suffix.
766
767         Note that this method can be overridden dynamically for generated
768         files that need different behavior.  See Tool/swig.py for
769         an example.
770         """
771         return self.dir.Entry(prefix + splitext(self.name)[0] + suffix)
772
773     def _Rfindalldirs_key(self, pathlist):
774         return pathlist
775
776     memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key))
777
778     def Rfindalldirs(self, pathlist):
779         """
780         Return all of the directories for a given path list, including
781         corresponding "backing" directories in any repositories.
782
783         The Node lookups are relative to this Node (typically a
784         directory), so memoizing result saves cycles from looking
785         up the same path for each target in a given directory.
786         """
787         try:
788             memo_dict = self._memo['Rfindalldirs']
789         except KeyError:
790             memo_dict = {}
791             self._memo['Rfindalldirs'] = memo_dict
792         else:
793             try:
794                 return memo_dict[pathlist]
795             except KeyError:
796                 pass
797
798         create_dir_relative_to_self = self.Dir
799         result = []
800         for path in pathlist:
801             if isinstance(path, SCons.Node.Node):
802                 result.append(path)
803             else:
804                 dir = create_dir_relative_to_self(path)
805                 result.extend(dir.get_all_rdirs())
806
807         memo_dict[pathlist] = result
808
809         return result
810
811     def RDirs(self, pathlist):
812         """Search for a list of directories in the Repository list."""
813         cwd = self.cwd or self.fs._cwd
814         return cwd.Rfindalldirs(pathlist)
815
816     memoizer_counters.append(SCons.Memoize.CountValue('rentry'))
817
818     def rentry(self):
819         try:
820             return self._memo['rentry']
821         except KeyError:
822             pass
823         result = self
824         if not self.exists():
825             norm_name = _my_normcase(self.name)
826             for dir in self.dir.get_all_rdirs():
827                 try:
828                     node = dir.entries[norm_name]
829                 except KeyError:
830                     if dir.entry_exists_on_disk(self.name):
831                         result = dir.Entry(self.name)
832                         break
833         self._memo['rentry'] = result
834         return result
835
836     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
837         return []
838
839 class Entry(Base):
840     """This is the class for generic Node.FS entries--that is, things
841     that could be a File or a Dir, but we're just not sure yet.
842     Consequently, the methods in this class really exist just to
843     transform their associated object into the right class when the
844     time comes, and then call the same-named method in the transformed
845     class."""
846
847     def diskcheck_match(self):
848         pass
849
850     def disambiguate(self, must_exist=None):
851         """
852         """
853         if self.isdir():
854             self.__class__ = Dir
855             self._morph()
856         elif self.isfile():
857             self.__class__ = File
858             self._morph()
859             self.clear()
860         else:
861             # There was nothing on-disk at this location, so look in
862             # the src directory.
863             #
864             # We can't just use self.srcnode() straight away because
865             # that would create an actual Node for this file in the src
866             # directory, and there might not be one.  Instead, use the
867             # dir_on_disk() method to see if there's something on-disk
868             # with that name, in which case we can go ahead and call
869             # self.srcnode() to create the right type of entry.
870             srcdir = self.dir.srcnode()
871             if srcdir != self.dir and \
872                srcdir.entry_exists_on_disk(self.name) and \
873                self.srcnode().isdir():
874                 self.__class__ = Dir
875                 self._morph()
876             elif must_exist:
877                 msg = "No such file or directory: '%s'" % self.abspath
878                 raise SCons.Errors.UserError, msg
879             else:
880                 self.__class__ = File
881                 self._morph()
882                 self.clear()
883         return self
884
885     def rfile(self):
886         """We're a generic Entry, but the caller is actually looking for
887         a File at this point, so morph into one."""
888         self.__class__ = File
889         self._morph()
890         self.clear()
891         return File.rfile(self)
892
893     def scanner_key(self):
894         return self.get_suffix()
895
896     def get_contents(self):
897         """Fetch the contents of the entry.  Returns the exact binary
898         contents of the file."""
899         try:
900             self = self.disambiguate(must_exist=1)
901         except SCons.Errors.UserError:
902             # There was nothing on disk with which to disambiguate
903             # this entry.  Leave it as an Entry, but return a null
904             # string so calls to get_contents() in emitters and the
905             # like (e.g. in qt.py) don't have to disambiguate by hand
906             # or catch the exception.
907             return ''
908         else:
909             return self.get_contents()
910
911     def get_text_contents(self):
912         """Fetch the decoded text contents of a Unicode encoded Entry.
913
914         Since this should return the text contents from the file
915         system, we check to see into what sort of subclass we should
916         morph this Entry."""
917         try:
918             self = self.disambiguate(must_exist=1)
919         except SCons.Errors.UserError:
920             # There was nothing on disk with which to disambiguate
921             # this entry.  Leave it as an Entry, but return a null
922             # string so calls to get_text_contents() in emitters and
923             # the like (e.g. in qt.py) don't have to disambiguate by
924             # hand or catch the exception.
925             return ''
926         else:
927             return self.get_text_contents()
928
929     def must_be_same(self, klass):
930         """Called to make sure a Node is a Dir.  Since we're an
931         Entry, we can morph into one."""
932         if self.__class__ is not klass:
933             self.__class__ = klass
934             self._morph()
935             self.clear()
936
937     # The following methods can get called before the Taskmaster has
938     # had a chance to call disambiguate() directly to see if this Entry
939     # should really be a Dir or a File.  We therefore use these to call
940     # disambiguate() transparently (from our caller's point of view).
941     #
942     # Right now, this minimal set of methods has been derived by just
943     # looking at some of the methods that will obviously be called early
944     # in any of the various Taskmasters' calling sequences, and then
945     # empirically figuring out which additional methods are necessary
946     # to make various tests pass.
947
948     def exists(self):
949         """Return if the Entry exists.  Check the file system to see
950         what we should turn into first.  Assume a file if there's no
951         directory."""
952         return self.disambiguate().exists()
953
954     def rel_path(self, other):
955         d = self.disambiguate()
956         if d.__class__ is Entry:
957             raise "rel_path() could not disambiguate File/Dir"
958         return d.rel_path(other)
959
960     def new_ninfo(self):
961         return self.disambiguate().new_ninfo()
962
963     def changed_since_last_build(self, target, prev_ni):
964         return self.disambiguate().changed_since_last_build(target, prev_ni)
965
966     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
967         return self.disambiguate()._glob1(pattern, ondisk, source, strings)
968
969     def get_subst_proxy(self):
970         return self.disambiguate().get_subst_proxy()
971
972 # This is for later so we can differentiate between Entry the class and Entry
973 # the method of the FS class.
974 _classEntry = Entry
975
976
977 class LocalFS:
978
979     if SCons.Memoize.use_memoizer:
980         __metaclass__ = SCons.Memoize.Memoized_Metaclass
981
982     # This class implements an abstraction layer for operations involving
983     # a local file system.  Essentially, this wraps any function in
984     # the os, os.path or shutil modules that we use to actually go do
985     # anything with or to the local file system.
986     #
987     # Note that there's a very good chance we'll refactor this part of
988     # the architecture in some way as we really implement the interface(s)
989     # for remote file system Nodes.  For example, the right architecture
990     # might be to have this be a subclass instead of a base class.
991     # Nevertheless, we're using this as a first step in that direction.
992     #
993     # We're not using chdir() yet because the calling subclass method
994     # needs to use os.chdir() directly to avoid recursion.  Will we
995     # really need this one?
996     #def chdir(self, path):
997     #    return os.chdir(path)
998     def chmod(self, path, mode):
999         return os.chmod(path, mode)
1000     def copy(self, src, dst):
1001         return shutil.copy(src, dst)
1002     def copy2(self, src, dst):
1003         return shutil.copy2(src, dst)
1004     def exists(self, path):
1005         return os.path.exists(path)
1006     def getmtime(self, path):
1007         return os.path.getmtime(path)
1008     def getsize(self, path):
1009         return os.path.getsize(path)
1010     def isdir(self, path):
1011         return os.path.isdir(path)
1012     def isfile(self, path):
1013         return os.path.isfile(path)
1014     def link(self, src, dst):
1015         return os.link(src, dst)
1016     def lstat(self, path):
1017         return os.lstat(path)
1018     def listdir(self, path):
1019         return os.listdir(path)
1020     def makedirs(self, path):
1021         return os.makedirs(path)
1022     def mkdir(self, path):
1023         return os.mkdir(path)
1024     def rename(self, old, new):
1025         return os.rename(old, new)
1026     def stat(self, path):
1027         return os.stat(path)
1028     def symlink(self, src, dst):
1029         return os.symlink(src, dst)
1030     def open(self, path):
1031         return open(path)
1032     def unlink(self, path):
1033         return os.unlink(path)
1034
1035     if hasattr(os, 'symlink'):
1036         def islink(self, path):
1037             return os.path.islink(path)
1038     else:
1039         def islink(self, path):
1040             return 0                    # no symlinks
1041
1042     if hasattr(os, 'readlink'):
1043         def readlink(self, file):
1044             return os.readlink(file)
1045     else:
1046         def readlink(self, file):
1047             return ''
1048
1049
1050 #class RemoteFS:
1051 #    # Skeleton for the obvious methods we might need from the
1052 #    # abstraction layer for a remote filesystem.
1053 #    def upload(self, local_src, remote_dst):
1054 #        pass
1055 #    def download(self, remote_src, local_dst):
1056 #        pass
1057
1058
1059 class FS(LocalFS):
1060
1061     memoizer_counters = []
1062
1063     def __init__(self, path = None):
1064         """Initialize the Node.FS subsystem.
1065
1066         The supplied path is the top of the source tree, where we
1067         expect to find the top-level build file.  If no path is
1068         supplied, the current directory is the default.
1069
1070         The path argument must be a valid absolute path.
1071         """
1072         if __debug__: logInstanceCreation(self, 'Node.FS')
1073
1074         self._memo = {}
1075
1076         self.Root = {}
1077         self.SConstruct_dir = None
1078         self.max_drift = default_max_drift
1079
1080         self.Top = None
1081         if path is None:
1082             self.pathTop = os.getcwd()
1083         else:
1084             self.pathTop = path
1085         self.defaultDrive = _my_normcase(os.path.splitdrive(self.pathTop)[0])
1086
1087         self.Top = self.Dir(self.pathTop)
1088         self.Top.path = '.'
1089         self.Top.tpath = '.'
1090         self._cwd = self.Top
1091
1092         DirNodeInfo.fs = self
1093         FileNodeInfo.fs = self
1094     
1095     def set_SConstruct_dir(self, dir):
1096         self.SConstruct_dir = dir
1097
1098     def get_max_drift(self):
1099         return self.max_drift
1100
1101     def set_max_drift(self, max_drift):
1102         self.max_drift = max_drift
1103
1104     def getcwd(self):
1105         return self._cwd
1106
1107     def chdir(self, dir, change_os_dir=0):
1108         """Change the current working directory for lookups.
1109         If change_os_dir is true, we will also change the "real" cwd
1110         to match.
1111         """
1112         curr=self._cwd
1113         try:
1114             if dir is not None:
1115                 self._cwd = dir
1116                 if change_os_dir:
1117                     os.chdir(dir.abspath)
1118         except OSError:
1119             self._cwd = curr
1120             raise
1121
1122     def get_root(self, drive):
1123         """
1124         Returns the root directory for the specified drive, creating
1125         it if necessary.
1126         """
1127         drive = _my_normcase(drive)
1128         try:
1129             return self.Root[drive]
1130         except KeyError:
1131             root = RootDir(drive, self)
1132             self.Root[drive] = root
1133             if not drive:
1134                 self.Root[self.defaultDrive] = root
1135             elif drive == self.defaultDrive:
1136                 self.Root[''] = root
1137             return root
1138
1139     def _lookup(self, p, directory, fsclass, create=1):
1140         """
1141         The generic entry point for Node lookup with user-supplied data.
1142
1143         This translates arbitrary input into a canonical Node.FS object
1144         of the specified fsclass.  The general approach for strings is
1145         to turn it into a fully normalized absolute path and then call
1146         the root directory's lookup_abs() method for the heavy lifting.
1147
1148         If the path name begins with '#', it is unconditionally
1149         interpreted relative to the top-level directory of this FS.  '#'
1150         is treated as a synonym for the top-level SConstruct directory,
1151         much like '~' is treated as a synonym for the user's home
1152         directory in a UNIX shell.  So both '#foo' and '#/foo' refer
1153         to the 'foo' subdirectory underneath the top-level SConstruct
1154         directory.
1155
1156         If the path name is relative, then the path is looked up relative
1157         to the specified directory, or the current directory (self._cwd,
1158         typically the SConscript directory) if the specified directory
1159         is None.
1160         """
1161         if isinstance(p, Base):
1162             # It's already a Node.FS object.  Make sure it's the right
1163             # class and return.
1164             p.must_be_same(fsclass)
1165             return p
1166         # str(p) in case it's something like a proxy object
1167         p = str(p)
1168
1169         initial_hash = (p[0:1] == '#')
1170         if initial_hash:
1171             # There was an initial '#', so we strip it and override
1172             # whatever directory they may have specified with the
1173             # top-level SConstruct directory.
1174             p = p[1:]
1175             directory = self.Top
1176
1177         if directory and not isinstance(directory, Dir):
1178             directory = self.Dir(directory)
1179
1180         if do_splitdrive:
1181             drive, p = os.path.splitdrive(p)
1182         else:
1183             drive = ''
1184         if drive and not p:
1185             # This causes a naked drive letter to be treated as a synonym
1186             # for the root directory on that drive.
1187             p = os.sep
1188         absolute = os.path.isabs(p)
1189
1190         needs_normpath = needs_normpath_check.match(p)
1191
1192         if initial_hash or not absolute:
1193             # This is a relative lookup, either to the top-level
1194             # SConstruct directory (because of the initial '#') or to
1195             # the current directory (the path name is not absolute).
1196             # Add the string to the appropriate directory lookup path,
1197             # after which the whole thing gets normalized.
1198             if not directory:
1199                 directory = self._cwd
1200             if p:
1201                 p = directory.labspath + '/' + p
1202             else:
1203                 p = directory.labspath
1204
1205         if needs_normpath:
1206             p = os.path.normpath(p)
1207
1208         if drive or absolute:
1209             root = self.get_root(drive)
1210         else:
1211             if not directory:
1212                 directory = self._cwd
1213             root = directory.root
1214
1215         if os.sep != '/':
1216             p = string.replace(p, os.sep, '/')
1217         return root._lookup_abs(p, fsclass, create)
1218
1219     def Entry(self, name, directory = None, create = 1):
1220         """Look up or create a generic Entry node with the specified name.
1221         If the name is a relative path (begins with ./, ../, or a file
1222         name), then it is looked up relative to the supplied directory
1223         node, or to the top level directory of the FS (supplied at
1224         construction time) if no directory is supplied.
1225         """
1226         return self._lookup(name, directory, Entry, create)
1227
1228     def File(self, name, directory = None, create = 1):
1229         """Look up or create a File node with the specified name.  If
1230         the name is a relative path (begins with ./, ../, or a file name),
1231         then it is looked up relative to the supplied directory node,
1232         or to the top level directory of the FS (supplied at construction
1233         time) if no directory is supplied.
1234
1235         This method will raise TypeError if a directory is found at the
1236         specified path.
1237         """
1238         return self._lookup(name, directory, File, create)
1239
1240     def Dir(self, name, directory = None, create = True):
1241         """Look up or create a Dir node with the specified name.  If
1242         the name is a relative path (begins with ./, ../, or a file name),
1243         then it is looked up relative to the supplied directory node,
1244         or to the top level directory of the FS (supplied at construction
1245         time) if no directory is supplied.
1246
1247         This method will raise TypeError if a normal file is found at the
1248         specified path.
1249         """
1250         return self._lookup(name, directory, Dir, create)
1251
1252     def VariantDir(self, variant_dir, src_dir, duplicate=1):
1253         """Link the supplied variant directory to the source directory
1254         for purposes of building files."""
1255
1256         if not isinstance(src_dir, SCons.Node.Node):
1257             src_dir = self.Dir(src_dir)
1258         if not isinstance(variant_dir, SCons.Node.Node):
1259             variant_dir = self.Dir(variant_dir)
1260         if src_dir.is_under(variant_dir):
1261             raise SCons.Errors.UserError, "Source directory cannot be under variant directory."
1262         if variant_dir.srcdir:
1263             if variant_dir.srcdir == src_dir:
1264                 return # We already did this.
1265             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir)
1266         variant_dir.link(src_dir, duplicate)
1267
1268     def Repository(self, *dirs):
1269         """Specify Repository directories to search."""
1270         for d in dirs:
1271             if not isinstance(d, SCons.Node.Node):
1272                 d = self.Dir(d)
1273             self.Top.addRepository(d)
1274
1275     def variant_dir_target_climb(self, orig, dir, tail):
1276         """Create targets in corresponding variant directories
1277
1278         Climb the directory tree, and look up path names
1279         relative to any linked variant directories we find.
1280
1281         Even though this loops and walks up the tree, we don't memoize
1282         the return value because this is really only used to process
1283         the command-line targets.
1284         """
1285         targets = []
1286         message = None
1287         fmt = "building associated VariantDir targets: %s"
1288         start_dir = dir
1289         while dir:
1290             for bd in dir.variant_dirs:
1291                 if start_dir.is_under(bd):
1292                     # If already in the build-dir location, don't reflect
1293                     return [orig], fmt % str(orig)
1294                 p = apply(os.path.join, [bd.path] + tail)
1295                 targets.append(self.Entry(p))
1296             tail = [dir.name] + tail
1297             dir = dir.up()
1298         if targets:
1299             message = fmt % string.join(map(str, targets))
1300         return targets, message
1301
1302     def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
1303         """
1304         Globs
1305
1306         This is mainly a shim layer 
1307         """
1308         if cwd is None:
1309             cwd = self.getcwd()
1310         return cwd.glob(pathname, ondisk, source, strings)
1311
1312 class DirNodeInfo(SCons.Node.NodeInfoBase):
1313     # This should get reset by the FS initialization.
1314     current_version_id = 1
1315
1316     fs = None
1317
1318     def str_to_node(self, s):
1319         top = self.fs.Top
1320         root = top.root
1321         if do_splitdrive:
1322             drive, s = os.path.splitdrive(s)
1323             if drive:
1324                 root = self.fs.get_root(drive)
1325         if not os.path.isabs(s):
1326             s = top.labspath + '/' + s
1327         return root._lookup_abs(s, Entry)
1328
1329 class DirBuildInfo(SCons.Node.BuildInfoBase):
1330     current_version_id = 1
1331
1332 glob_magic_check = re.compile('[*?[]')
1333
1334 def has_glob_magic(s):
1335     return glob_magic_check.search(s) is not None
1336
1337 class Dir(Base):
1338     """A class for directories in a file system.
1339     """
1340
1341     memoizer_counters = []
1342
1343     NodeInfo = DirNodeInfo
1344     BuildInfo = DirBuildInfo
1345
1346     def __init__(self, name, directory, fs):
1347         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1348         Base.__init__(self, name, directory, fs)
1349         self._morph()
1350
1351     def _morph(self):
1352         """Turn a file system Node (either a freshly initialized directory
1353         object or a separate Entry object) into a proper directory object.
1354
1355         Set up this directory's entries and hook it into the file
1356         system tree.  Specify that directories (this Node) don't use
1357         signatures for calculating whether they're current.
1358         """
1359
1360         self.repositories = []
1361         self.srcdir = None
1362
1363         self.entries = {}
1364         self.entries['.'] = self
1365         self.entries['..'] = self.dir
1366         self.cwd = self
1367         self.searched = 0
1368         self._sconsign = None
1369         self.variant_dirs = []
1370         self.root = self.dir.root
1371
1372         # Don't just reset the executor, replace its action list,
1373         # because it might have some pre-or post-actions that need to
1374         # be preserved.
1375         self.builder = get_MkdirBuilder()
1376         self.get_executor().set_action_list(self.builder.action)
1377
1378     def diskcheck_match(self):
1379         diskcheck_match(self, self.isfile,
1380                         "File %s found where directory expected.")
1381
1382     def __clearRepositoryCache(self, duplicate=None):
1383         """Called when we change the repository(ies) for a directory.
1384         This clears any cached information that is invalidated by changing
1385         the repository."""
1386
1387         for node in self.entries.values():
1388             if node != self.dir:
1389                 if node != self and isinstance(node, Dir):
1390                     node.__clearRepositoryCache(duplicate)
1391                 else:
1392                     node.clear()
1393                     try:
1394                         del node._srcreps
1395                     except AttributeError:
1396                         pass
1397                     if duplicate is not None:
1398                         node.duplicate=duplicate
1399
1400     def __resetDuplicate(self, node):
1401         if node != self:
1402             node.duplicate = node.get_dir().duplicate
1403
1404     def Entry(self, name):
1405         """
1406         Looks up or creates an entry node named 'name' relative to
1407         this directory.
1408         """
1409         return self.fs.Entry(name, self)
1410
1411     def Dir(self, name, create=True):
1412         """
1413         Looks up or creates a directory node named 'name' relative to
1414         this directory.
1415         """
1416         return self.fs.Dir(name, self, create)
1417
1418     def File(self, name):
1419         """
1420         Looks up or creates a file node named 'name' relative to
1421         this directory.
1422         """
1423         return self.fs.File(name, self)
1424
1425     def _lookup_rel(self, name, klass, create=1):
1426         """
1427         Looks up a *normalized* relative path name, relative to this
1428         directory.
1429
1430         This method is intended for use by internal lookups with
1431         already-normalized path data.  For general-purpose lookups,
1432         use the Entry(), Dir() and File() methods above.
1433
1434         This method does *no* input checking and will die or give
1435         incorrect results if it's passed a non-normalized path name (e.g.,
1436         a path containing '..'), an absolute path name, a top-relative
1437         ('#foo') path name, or any kind of object.
1438         """
1439         name = self.entry_labspath(name)
1440         return self.root._lookup_abs(name, klass, create)
1441
1442     def link(self, srcdir, duplicate):
1443         """Set this directory as the variant directory for the
1444         supplied source directory."""
1445         self.srcdir = srcdir
1446         self.duplicate = duplicate
1447         self.__clearRepositoryCache(duplicate)
1448         srcdir.variant_dirs.append(self)
1449
1450     def getRepositories(self):
1451         """Returns a list of repositories for this directory.
1452         """
1453         if self.srcdir and not self.duplicate:
1454             return self.srcdir.get_all_rdirs() + self.repositories
1455         return self.repositories
1456
1457     memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
1458
1459     def get_all_rdirs(self):
1460         try:
1461             return list(self._memo['get_all_rdirs'])
1462         except KeyError:
1463             pass
1464
1465         result = [self]
1466         fname = '.'
1467         dir = self
1468         while dir:
1469             for rep in dir.getRepositories():
1470                 result.append(rep.Dir(fname))
1471             if fname == '.':
1472                 fname = dir.name
1473             else:
1474                 fname = dir.name + os.sep + fname
1475             dir = dir.up()
1476
1477         self._memo['get_all_rdirs'] = list(result)
1478
1479         return result
1480
1481     def addRepository(self, dir):
1482         if dir != self and not dir in self.repositories:
1483             self.repositories.append(dir)
1484             dir.tpath = '.'
1485             self.__clearRepositoryCache()
1486
1487     def up(self):
1488         return self.entries['..']
1489
1490     def _rel_path_key(self, other):
1491         return str(other)
1492
1493     memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
1494
1495     def rel_path(self, other):
1496         """Return a path to "other" relative to this directory.
1497         """
1498
1499         # This complicated and expensive method, which constructs relative
1500         # paths between arbitrary Node.FS objects, is no longer used
1501         # by SCons itself.  It was introduced to store dependency paths
1502         # in .sconsign files relative to the target, but that ended up
1503         # being significantly inefficient.
1504         #
1505         # We're continuing to support the method because some SConstruct
1506         # files out there started using it when it was available, and
1507         # we're all about backwards compatibility..
1508
1509         try:
1510             memo_dict = self._memo['rel_path']
1511         except KeyError:
1512             memo_dict = {}
1513             self._memo['rel_path'] = memo_dict
1514         else:
1515             try:
1516                 return memo_dict[other]
1517             except KeyError:
1518                 pass
1519
1520         if self is other:
1521             result = '.'
1522
1523         elif not other in self.path_elements:
1524             try:
1525                 other_dir = other.get_dir()
1526             except AttributeError:
1527                 result = str(other)
1528             else:
1529                 if other_dir is None:
1530                     result = other.name
1531                 else:
1532                     dir_rel_path = self.rel_path(other_dir)
1533                     if dir_rel_path == '.':
1534                         result = other.name
1535                     else:
1536                         result = dir_rel_path + os.sep + other.name
1537         else:
1538             i = self.path_elements.index(other) + 1
1539
1540             path_elems = ['..'] * (len(self.path_elements) - i) \
1541                          + map(lambda n: n.name, other.path_elements[i:])
1542              
1543             result = string.join(path_elems, os.sep)
1544
1545         memo_dict[other] = result
1546
1547         return result
1548
1549     def get_env_scanner(self, env, kw={}):
1550         import SCons.Defaults
1551         return SCons.Defaults.DirEntryScanner
1552
1553     def get_target_scanner(self):
1554         import SCons.Defaults
1555         return SCons.Defaults.DirEntryScanner
1556
1557     def get_found_includes(self, env, scanner, path):
1558         """Return this directory's implicit dependencies.
1559
1560         We don't bother caching the results because the scan typically
1561         shouldn't be requested more than once (as opposed to scanning
1562         .h file contents, which can be requested as many times as the
1563         files is #included by other files).
1564         """
1565         if not scanner:
1566             return []
1567         # Clear cached info for this Dir.  If we already visited this
1568         # directory on our walk down the tree (because we didn't know at
1569         # that point it was being used as the source for another Node)
1570         # then we may have calculated build signature before realizing
1571         # we had to scan the disk.  Now that we have to, though, we need
1572         # to invalidate the old calculated signature so that any node
1573         # dependent on our directory structure gets one that includes
1574         # info about everything on disk.
1575         self.clear()
1576         return scanner(self, env, path)
1577
1578     #
1579     # Taskmaster interface subsystem
1580     #
1581
1582     def prepare(self):
1583         pass
1584
1585     def build(self, **kw):
1586         """A null "builder" for directories."""
1587         global MkdirBuilder
1588         if self.builder is not MkdirBuilder:
1589             apply(SCons.Node.Node.build, [self,], kw)
1590
1591     #
1592     #
1593     #
1594
1595     def _create(self):
1596         """Create this directory, silently and without worrying about
1597         whether the builder is the default or not."""
1598         listDirs = []
1599         parent = self
1600         while parent:
1601             if parent.exists():
1602                 break
1603             listDirs.append(parent)
1604             parent = parent.up()
1605         else:
1606             raise SCons.Errors.StopError, parent.path
1607         listDirs.reverse()
1608         for dirnode in listDirs:
1609             try:
1610                 # Don't call dirnode.build(), call the base Node method
1611                 # directly because we definitely *must* create this
1612                 # directory.  The dirnode.build() method will suppress
1613                 # the build if it's the default builder.
1614                 SCons.Node.Node.build(dirnode)
1615                 dirnode.get_executor().nullify()
1616                 # The build() action may or may not have actually
1617                 # created the directory, depending on whether the -n
1618                 # option was used or not.  Delete the _exists and
1619                 # _rexists attributes so they can be reevaluated.
1620                 dirnode.clear()
1621             except OSError:
1622                 pass
1623
1624     def multiple_side_effect_has_builder(self):
1625         global MkdirBuilder
1626         return self.builder is not MkdirBuilder and self.has_builder()
1627
1628     def alter_targets(self):
1629         """Return any corresponding targets in a variant directory.
1630         """
1631         return self.fs.variant_dir_target_climb(self, self, [])
1632
1633     def scanner_key(self):
1634         """A directory does not get scanned."""
1635         return None
1636
1637     def get_text_contents(self):
1638         """We already emit things in text, so just return the binary
1639         version."""
1640         return self.get_contents()
1641
1642     def get_contents(self):
1643         """Return content signatures and names of all our children
1644         separated by new-lines. Ensure that the nodes are sorted."""
1645         contents = []
1646         name_cmp = lambda a, b: cmp(a.name, b.name)
1647         sorted_children = self.children()[:]
1648         sorted_children.sort(name_cmp)
1649         for node in sorted_children:
1650             contents.append('%s %s\n' % (node.get_csig(), node.name))
1651         return string.join(contents, '')
1652
1653     def get_csig(self):
1654         """Compute the content signature for Directory nodes. In
1655         general, this is not needed and the content signature is not
1656         stored in the DirNodeInfo. However, if get_contents on a Dir
1657         node is called which has a child directory, the child
1658         directory should return the hash of its contents."""
1659         contents = self.get_contents()
1660         return SCons.Util.MD5signature(contents)
1661
1662     def do_duplicate(self, src):
1663         pass
1664
1665     changed_since_last_build = SCons.Node.Node.state_has_changed
1666
1667     def is_up_to_date(self):
1668         """If any child is not up-to-date, then this directory isn't,
1669         either."""
1670         if self.builder is not MkdirBuilder and not self.exists():
1671             return 0
1672         up_to_date = SCons.Node.up_to_date
1673         for kid in self.children():
1674             if kid.get_state() > up_to_date:
1675                 return 0
1676         return 1
1677
1678     def rdir(self):
1679         if not self.exists():
1680             norm_name = _my_normcase(self.name)
1681             for dir in self.dir.get_all_rdirs():
1682                 try: node = dir.entries[norm_name]
1683                 except KeyError: node = dir.dir_on_disk(self.name)
1684                 if node and node.exists() and \
1685                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1686                         return node
1687         return self
1688
1689     def sconsign(self):
1690         """Return the .sconsign file info for this directory,
1691         creating it first if necessary."""
1692         if not self._sconsign:
1693             import SCons.SConsign
1694             self._sconsign = SCons.SConsign.ForDirectory(self)
1695         return self._sconsign
1696
1697     def srcnode(self):
1698         """Dir has a special need for srcnode()...if we
1699         have a srcdir attribute set, then that *is* our srcnode."""
1700         if self.srcdir:
1701             return self.srcdir
1702         return Base.srcnode(self)
1703
1704     def get_timestamp(self):
1705         """Return the latest timestamp from among our children"""
1706         stamp = 0
1707         for kid in self.children():
1708             if kid.get_timestamp() > stamp:
1709                 stamp = kid.get_timestamp()
1710         return stamp
1711
1712     def entry_abspath(self, name):
1713         return self.abspath + os.sep + name
1714
1715     def entry_labspath(self, name):
1716         return self.labspath + '/' + name
1717
1718     def entry_path(self, name):
1719         return self.path + os.sep + name
1720
1721     def entry_tpath(self, name):
1722         return self.tpath + os.sep + name
1723
1724     def entry_exists_on_disk(self, name):
1725         try:
1726             d = self.on_disk_entries
1727         except AttributeError:
1728             d = {}
1729             try:
1730                 entries = os.listdir(self.abspath)
1731             except OSError:
1732                 pass
1733             else:
1734                 for entry in map(_my_normcase, entries):
1735                     d[entry] = 1
1736             self.on_disk_entries = d
1737         return d.has_key(_my_normcase(name))
1738
1739     memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
1740
1741     def srcdir_list(self):
1742         try:
1743             return self._memo['srcdir_list']
1744         except KeyError:
1745             pass
1746
1747         result = []
1748
1749         dirname = '.'
1750         dir = self
1751         while dir:
1752             if dir.srcdir:
1753                 result.append(dir.srcdir.Dir(dirname))
1754             dirname = dir.name + os.sep + dirname
1755             dir = dir.up()
1756
1757         self._memo['srcdir_list'] = result
1758
1759         return result
1760
1761     def srcdir_duplicate(self, name):
1762         for dir in self.srcdir_list():
1763             if self.is_under(dir):
1764                 # We shouldn't source from something in the build path;
1765                 # variant_dir is probably under src_dir, in which case
1766                 # we are reflecting.
1767                 break
1768             if dir.entry_exists_on_disk(name):
1769                 srcnode = dir.Entry(name).disambiguate()
1770                 if self.duplicate:
1771                     node = self.Entry(name).disambiguate()
1772                     node.do_duplicate(srcnode)
1773                     return node
1774                 else:
1775                     return srcnode
1776         return None
1777
1778     def _srcdir_find_file_key(self, filename):
1779         return filename
1780
1781     memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
1782
1783     def srcdir_find_file(self, filename):
1784         try:
1785             memo_dict = self._memo['srcdir_find_file']
1786         except KeyError:
1787             memo_dict = {}
1788             self._memo['srcdir_find_file'] = memo_dict
1789         else:
1790             try:
1791                 return memo_dict[filename]
1792             except KeyError:
1793                 pass
1794
1795         def func(node):
1796             if (isinstance(node, File) or isinstance(node, Entry)) and \
1797                (node.is_derived() or node.exists()):
1798                     return node
1799             return None
1800
1801         norm_name = _my_normcase(filename)
1802
1803         for rdir in self.get_all_rdirs():
1804             try: node = rdir.entries[norm_name]
1805             except KeyError: node = rdir.file_on_disk(filename)
1806             else: node = func(node)
1807             if node:
1808                 result = (node, self)
1809                 memo_dict[filename] = result
1810                 return result
1811
1812         for srcdir in self.srcdir_list():
1813             for rdir in srcdir.get_all_rdirs():
1814                 try: node = rdir.entries[norm_name]
1815                 except KeyError: node = rdir.file_on_disk(filename)
1816                 else: node = func(node)
1817                 if node:
1818                     result = (File(filename, self, self.fs), srcdir)
1819                     memo_dict[filename] = result
1820                     return result
1821
1822         result = (None, None)
1823         memo_dict[filename] = result
1824         return result
1825
1826     def dir_on_disk(self, name):
1827         if self.entry_exists_on_disk(name):
1828             try: return self.Dir(name)
1829             except TypeError: pass
1830         node = self.srcdir_duplicate(name)
1831         if isinstance(node, File):
1832             return None
1833         return node
1834
1835     def file_on_disk(self, name):
1836         if self.entry_exists_on_disk(name) or \
1837            diskcheck_rcs(self, name) or \
1838            diskcheck_sccs(self, name):
1839             try: return self.File(name)
1840             except TypeError: pass
1841         node = self.srcdir_duplicate(name)
1842         if isinstance(node, Dir):
1843             return None
1844         return node
1845
1846     def walk(self, func, arg):
1847         """
1848         Walk this directory tree by calling the specified function
1849         for each directory in the tree.
1850
1851         This behaves like the os.path.walk() function, but for in-memory
1852         Node.FS.Dir objects.  The function takes the same arguments as
1853         the functions passed to os.path.walk():
1854
1855                 func(arg, dirname, fnames)
1856
1857         Except that "dirname" will actually be the directory *Node*,
1858         not the string.  The '.' and '..' entries are excluded from
1859         fnames.  The fnames list may be modified in-place to filter the
1860         subdirectories visited or otherwise impose a specific order.
1861         The "arg" argument is always passed to func() and may be used
1862         in any way (or ignored, passing None is common).
1863         """
1864         entries = self.entries
1865         names = entries.keys()
1866         names.remove('.')
1867         names.remove('..')
1868         func(arg, self, names)
1869         select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
1870         for dirname in filter(select_dirs, names):
1871             entries[dirname].walk(func, arg)
1872
1873     def glob(self, pathname, ondisk=True, source=False, strings=False):
1874         """
1875         Returns a list of Nodes (or strings) matching a specified
1876         pathname pattern.
1877
1878         Pathname patterns follow UNIX shell semantics:  * matches
1879         any-length strings of any characters, ? matches any character,
1880         and [] can enclose lists or ranges of characters.  Matches do
1881         not span directory separators.
1882
1883         The matches take into account Repositories, returning local
1884         Nodes if a corresponding entry exists in a Repository (either
1885         an in-memory Node or something on disk).
1886
1887         By defafult, the glob() function matches entries that exist
1888         on-disk, in addition to in-memory Nodes.  Setting the "ondisk"
1889         argument to False (or some other non-true value) causes the glob()
1890         function to only match in-memory Nodes.  The default behavior is
1891         to return both the on-disk and in-memory Nodes.
1892
1893         The "source" argument, when true, specifies that corresponding
1894         source Nodes must be returned if you're globbing in a build
1895         directory (initialized with VariantDir()).  The default behavior
1896         is to return Nodes local to the VariantDir().
1897
1898         The "strings" argument, when true, returns the matches as strings,
1899         not Nodes.  The strings are path names relative to this directory.
1900
1901         The underlying algorithm is adapted from the glob.glob() function
1902         in the Python library (but heavily modified), and uses fnmatch()
1903         under the covers.
1904         """
1905         dirname, basename = os.path.split(pathname)
1906         if not dirname:
1907             return self._glob1(basename, ondisk, source, strings)
1908         if has_glob_magic(dirname):
1909             list = self.glob(dirname, ondisk, source, strings=False)
1910         else:
1911             list = [self.Dir(dirname, create=True)]
1912         result = []
1913         for dir in list:
1914             r = dir._glob1(basename, ondisk, source, strings)
1915             if strings:
1916                 r = map(lambda x, d=str(dir): os.path.join(d, x), r)
1917             result.extend(r)
1918         result.sort(lambda a, b: cmp(str(a), str(b)))
1919         return result
1920
1921     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
1922         """
1923         Globs for and returns a list of entry names matching a single
1924         pattern in this directory.
1925
1926         This searches any repositories and source directories for
1927         corresponding entries and returns a Node (or string) relative
1928         to the current directory if an entry is found anywhere.
1929
1930         TODO: handle pattern with no wildcard
1931         """
1932         search_dir_list = self.get_all_rdirs()
1933         for srcdir in self.srcdir_list():
1934             search_dir_list.extend(srcdir.get_all_rdirs())
1935
1936         selfEntry = self.Entry
1937         names = []
1938         for dir in search_dir_list:
1939             # We use the .name attribute from the Node because the keys of
1940             # the dir.entries dictionary are normalized (that is, all upper
1941             # case) on case-insensitive systems like Windows.
1942             #node_names = [ v.name for k, v in dir.entries.items() if k not in ('.', '..') ]
1943             entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys())
1944             node_names = map(lambda n, e=dir.entries: e[n].name, entry_names)
1945             names.extend(node_names)
1946             if not strings:
1947                 # Make sure the working directory (self) actually has
1948                 # entries for all Nodes in repositories or variant dirs.
1949                 map(selfEntry, node_names)
1950             if ondisk:
1951                 try:
1952                     disk_names = os.listdir(dir.abspath)
1953                 except os.error:
1954                     continue
1955                 names.extend(disk_names)
1956                 if not strings:
1957                     # We're going to return corresponding Nodes in
1958                     # the local directory, so we need to make sure
1959                     # those Nodes exist.  We only want to create
1960                     # Nodes for the entries that will match the
1961                     # specified pattern, though, which means we
1962                     # need to filter the list here, even though
1963                     # the overall list will also be filtered later,
1964                     # after we exit this loop.
1965                     if pattern[0] != '.':
1966                         #disk_names = [ d for d in disk_names if d[0] != '.' ]
1967                         disk_names = filter(lambda x: x[0] != '.', disk_names)
1968                     disk_names = fnmatch.filter(disk_names, pattern)
1969                     dirEntry = dir.Entry
1970                     for name in disk_names:
1971                         # Add './' before disk filename so that '#' at
1972                         # beginning of filename isn't interpreted.
1973                         name = './' + name
1974                         node = dirEntry(name).disambiguate()
1975                         n = selfEntry(name)
1976                         if n.__class__ != node.__class__:
1977                             n.__class__ = node.__class__
1978                             n._morph()
1979
1980         names = set(names)
1981         if pattern[0] != '.':
1982             #names = [ n for n in names if n[0] != '.' ]
1983             names = filter(lambda x: x[0] != '.', names)
1984         names = fnmatch.filter(names, pattern)
1985
1986         if strings:
1987             return names
1988
1989         #return [ self.entries[_my_normcase(n)] for n in names ]
1990         return map(lambda n, e=self.entries:  e[_my_normcase(n)], names)
1991
1992 class RootDir(Dir):
1993     """A class for the root directory of a file system.
1994
1995     This is the same as a Dir class, except that the path separator
1996     ('/' or '\\') is actually part of the name, so we don't need to
1997     add a separator when creating the path names of entries within
1998     this directory.
1999     """
2000     def __init__(self, name, fs):
2001         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
2002         # We're going to be our own parent directory (".." entry and .dir
2003         # attribute) so we have to set up some values so Base.__init__()
2004         # won't gag won't it calls some of our methods.
2005         self.abspath = ''
2006         self.labspath = ''
2007         self.path = ''
2008         self.tpath = ''
2009         self.path_elements = []
2010         self.duplicate = 0
2011         self.root = self
2012         Base.__init__(self, name, self, fs)
2013
2014         # Now set our paths to what we really want them to be: the
2015         # initial drive letter (the name) plus the directory separator,
2016         # except for the "lookup abspath," which does not have the
2017         # drive letter.
2018         self.abspath = name + os.sep
2019         self.labspath = ''
2020         self.path = name + os.sep
2021         self.tpath = name + os.sep
2022         self._morph()
2023
2024         self._lookupDict = {}
2025
2026         # The // and os.sep + os.sep entries are necessary because
2027         # os.path.normpath() seems to preserve double slashes at the
2028         # beginning of a path (presumably for UNC path names), but
2029         # collapses triple slashes to a single slash.
2030         self._lookupDict[''] = self
2031         self._lookupDict['/'] = self
2032         self._lookupDict['//'] = self
2033         self._lookupDict[os.sep] = self
2034         self._lookupDict[os.sep + os.sep] = self
2035
2036     def must_be_same(self, klass):
2037         if klass is Dir:
2038             return
2039         Base.must_be_same(self, klass)
2040
2041     def _lookup_abs(self, p, klass, create=1):
2042         """
2043         Fast (?) lookup of a *normalized* absolute path.
2044
2045         This method is intended for use by internal lookups with
2046         already-normalized path data.  For general-purpose lookups,
2047         use the FS.Entry(), FS.Dir() or FS.File() methods.
2048
2049         The caller is responsible for making sure we're passed a
2050         normalized absolute path; we merely let Python's dictionary look
2051         up and return the One True Node.FS object for the path.
2052
2053         If no Node for the specified "p" doesn't already exist, and
2054         "create" is specified, the Node may be created after recursive
2055         invocation to find or create the parent directory or directories.
2056         """
2057         k = _my_normcase(p)
2058         try:
2059             result = self._lookupDict[k]
2060         except KeyError:
2061             if not create:
2062                 raise SCons.Errors.UserError
2063             # There is no Node for this path name, and we're allowed
2064             # to create it.
2065             dir_name, file_name = os.path.split(p)
2066             dir_node = self._lookup_abs(dir_name, Dir)
2067             result = klass(file_name, dir_node, self.fs)
2068
2069             # Double-check on disk (as configured) that the Node we
2070             # created matches whatever is out there in the real world.
2071             result.diskcheck_match()
2072
2073             self._lookupDict[k] = result
2074             dir_node.entries[_my_normcase(file_name)] = result
2075             dir_node.implicit = None
2076         else:
2077             # There is already a Node for this path name.  Allow it to
2078             # complain if we were looking for an inappropriate type.
2079             result.must_be_same(klass)
2080         return result
2081
2082     def __str__(self):
2083         return self.abspath
2084
2085     def entry_abspath(self, name):
2086         return self.abspath + name
2087
2088     def entry_labspath(self, name):
2089         return '/' + name
2090
2091     def entry_path(self, name):
2092         return self.path + name
2093
2094     def entry_tpath(self, name):
2095         return self.tpath + name
2096
2097     def is_under(self, dir):
2098         if self is dir:
2099             return 1
2100         else:
2101             return 0
2102
2103     def up(self):
2104         return None
2105
2106     def get_dir(self):
2107         return None
2108
2109     def src_builder(self):
2110         return _null
2111
2112 class FileNodeInfo(SCons.Node.NodeInfoBase):
2113     current_version_id = 1
2114
2115     field_list = ['csig', 'timestamp', 'size']
2116
2117     # This should get reset by the FS initialization.
2118     fs = None
2119
2120     def str_to_node(self, s):
2121         top = self.fs.Top
2122         root = top.root
2123         if do_splitdrive:
2124             drive, s = os.path.splitdrive(s)
2125             if drive:
2126                 root = self.fs.get_root(drive)
2127         if not os.path.isabs(s):
2128             s = top.labspath + '/' + s
2129         return root._lookup_abs(s, Entry)
2130
2131 class FileBuildInfo(SCons.Node.BuildInfoBase):
2132     current_version_id = 1
2133
2134     def convert_to_sconsign(self):
2135         """
2136         Converts this FileBuildInfo object for writing to a .sconsign file
2137
2138         This replaces each Node in our various dependency lists with its
2139         usual string representation: relative to the top-level SConstruct
2140         directory, or an absolute path if it's outside.
2141         """
2142         if os.sep == '/':
2143             node_to_str = str
2144         else:
2145             def node_to_str(n):
2146                 try:
2147                     s = n.path
2148                 except AttributeError:
2149                     s = str(n)
2150                 else:
2151                     s = string.replace(s, os.sep, '/')
2152                 return s
2153         for attr in ['bsources', 'bdepends', 'bimplicit']:
2154             try:
2155                 val = getattr(self, attr)
2156             except AttributeError:
2157                 pass
2158             else:
2159                 setattr(self, attr, map(node_to_str, val))
2160     def convert_from_sconsign(self, dir, name):
2161         """
2162         Converts a newly-read FileBuildInfo object for in-SCons use
2163
2164         For normal up-to-date checking, we don't have any conversion to
2165         perform--but we're leaving this method here to make that clear.
2166         """
2167         pass
2168     def prepare_dependencies(self):
2169         """
2170         Prepares a FileBuildInfo object for explaining what changed
2171
2172         The bsources, bdepends and bimplicit lists have all been
2173         stored on disk as paths relative to the top-level SConstruct
2174         directory.  Convert the strings to actual Nodes (for use by the
2175         --debug=explain code and --implicit-cache).
2176         """
2177         attrs = [
2178             ('bsources', 'bsourcesigs'),
2179             ('bdepends', 'bdependsigs'),
2180             ('bimplicit', 'bimplicitsigs'),
2181         ]
2182         for (nattr, sattr) in attrs:
2183             try:
2184                 strings = getattr(self, nattr)
2185                 nodeinfos = getattr(self, sattr)
2186             except AttributeError:
2187                 continue
2188             nodes = []
2189             for s, ni in izip(strings, nodeinfos):
2190                 if not isinstance(s, SCons.Node.Node):
2191                     s = ni.str_to_node(s)
2192                 nodes.append(s)
2193             setattr(self, nattr, nodes)
2194     def format(self, names=0):
2195         result = []
2196         bkids = self.bsources + self.bdepends + self.bimplicit
2197         bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
2198         for bkid, bkidsig in izip(bkids, bkidsigs):
2199             result.append(str(bkid) + ': ' +
2200                           string.join(bkidsig.format(names=names), ' '))
2201         result.append('%s [%s]' % (self.bactsig, self.bact))
2202         return string.join(result, '\n')
2203
2204 class File(Base):
2205     """A class for files in a file system.
2206     """
2207
2208     memoizer_counters = []
2209
2210     NodeInfo = FileNodeInfo
2211     BuildInfo = FileBuildInfo
2212
2213     md5_chunksize = 64
2214
2215     def diskcheck_match(self):
2216         diskcheck_match(self, self.isdir,
2217                         "Directory %s found where file expected.")
2218
2219     def __init__(self, name, directory, fs):
2220         if __debug__: logInstanceCreation(self, 'Node.FS.File')
2221         Base.__init__(self, name, directory, fs)
2222         self._morph()
2223
2224     def Entry(self, name):
2225         """Create an entry node named 'name' relative to
2226         the directory of this file."""
2227         return self.dir.Entry(name)
2228
2229     def Dir(self, name, create=True):
2230         """Create a directory node named 'name' relative to
2231         the directory of this file."""
2232         return self.dir.Dir(name, create=create)
2233
2234     def Dirs(self, pathlist):
2235         """Create a list of directories relative to the SConscript
2236         directory of this file."""
2237         # TODO(1.5)
2238         # return [self.Dir(p) for p in pathlist]
2239         return map(lambda p, s=self: s.Dir(p), pathlist)
2240
2241     def File(self, name):
2242         """Create a file node named 'name' relative to
2243         the directory of this file."""
2244         return self.dir.File(name)
2245
2246     #def generate_build_dict(self):
2247     #    """Return an appropriate dictionary of values for building
2248     #    this File."""
2249     #    return {'Dir' : self.Dir,
2250     #            'File' : self.File,
2251     #            'RDirs' : self.RDirs}
2252
2253     def _morph(self):
2254         """Turn a file system node into a File object."""
2255         self.scanner_paths = {}
2256         if not hasattr(self, '_local'):
2257             self._local = 0
2258
2259         # If there was already a Builder set on this entry, then
2260         # we need to make sure we call the target-decider function,
2261         # not the source-decider.  Reaching in and doing this by hand
2262         # is a little bogus.  We'd prefer to handle this by adding
2263         # an Entry.builder_set() method that disambiguates like the
2264         # other methods, but that starts running into problems with the
2265         # fragile way we initialize Dir Nodes with their Mkdir builders,
2266         # yet still allow them to be overridden by the user.  Since it's
2267         # not clear right now how to fix that, stick with what works
2268         # until it becomes clear...
2269         if self.has_builder():
2270             self.changed_since_last_build = self.decide_target
2271
2272     def scanner_key(self):
2273         return self.get_suffix()
2274
2275     def get_contents(self):
2276         if not self.rexists():
2277             return ''
2278         fname = self.rfile().abspath
2279         try:
2280             contents = open(fname, "rb").read()
2281         except EnvironmentError, e:
2282             if not e.filename:
2283                 e.filename = fname
2284             raise
2285         return contents
2286
2287     try:
2288         import codecs
2289     except ImportError:
2290         get_text_contents = get_contents
2291     else:
2292         # This attempts to figure out what the encoding of the text is
2293         # based upon the BOM bytes, and then decodes the contents so that
2294         # it's a valid python string.
2295         def get_text_contents(self):
2296             contents = self.get_contents()
2297             if contents.startswith(codecs.BOM_UTF8):
2298                 contents = contents.decode('utf-8')
2299             elif contents.startswith(codecs.BOM_UTF16):
2300                 contents = contents.decode('utf-16')
2301             return contents
2302
2303     def get_content_hash(self):
2304         """
2305         Compute and return the MD5 hash for this file.
2306         """
2307         if not self.rexists():
2308             return SCons.Util.MD5signature('')
2309         fname = self.rfile().abspath
2310         try:
2311             cs = SCons.Util.MD5filesignature(fname,
2312                 chunksize=SCons.Node.FS.File.md5_chunksize*1024)
2313         except EnvironmentError, e:
2314             if not e.filename:
2315                 e.filename = fname
2316             raise
2317         return cs
2318         
2319
2320     memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
2321
2322     def get_size(self):
2323         try:
2324             return self._memo['get_size']
2325         except KeyError:
2326             pass
2327
2328         if self.rexists():
2329             size = self.rfile().getsize()
2330         else:
2331             size = 0
2332
2333         self._memo['get_size'] = size
2334
2335         return size
2336
2337     memoizer_counters.append(SCons.Memoize.CountValue('get_timestamp'))
2338
2339     def get_timestamp(self):
2340         try:
2341             return self._memo['get_timestamp']
2342         except KeyError:
2343             pass
2344
2345         if self.rexists():
2346             timestamp = self.rfile().getmtime()
2347         else:
2348             timestamp = 0
2349
2350         self._memo['get_timestamp'] = timestamp
2351
2352         return timestamp
2353
2354     def store_info(self):
2355         # Merge our build information into the already-stored entry.
2356         # This accomodates "chained builds" where a file that's a target
2357         # in one build (SConstruct file) is a source in a different build.
2358         # See test/chained-build.py for the use case.
2359         if do_store_info:
2360             self.dir.sconsign().store_info(self.name, self)
2361
2362     convert_copy_attrs = [
2363         'bsources',
2364         'bimplicit',
2365         'bdepends',
2366         'bact',
2367         'bactsig',
2368         'ninfo',
2369     ]
2370
2371
2372     convert_sig_attrs = [
2373         'bsourcesigs',
2374         'bimplicitsigs',
2375         'bdependsigs',
2376     ]
2377
2378     def convert_old_entry(self, old_entry):
2379         # Convert a .sconsign entry from before the Big Signature
2380         # Refactoring, doing what we can to convert its information
2381         # to the new .sconsign entry format.
2382         #
2383         # The old format looked essentially like this:
2384         #
2385         #   BuildInfo
2386         #       .ninfo (NodeInfo)
2387         #           .bsig
2388         #           .csig
2389         #           .timestamp
2390         #           .size
2391         #       .bsources
2392         #       .bsourcesigs ("signature" list)
2393         #       .bdepends
2394         #       .bdependsigs ("signature" list)
2395         #       .bimplicit
2396         #       .bimplicitsigs ("signature" list)
2397         #       .bact
2398         #       .bactsig
2399         #
2400         # The new format looks like this:
2401         #
2402         #   .ninfo (NodeInfo)
2403         #       .bsig
2404         #       .csig
2405         #       .timestamp
2406         #       .size
2407         #   .binfo (BuildInfo)
2408         #       .bsources
2409         #       .bsourcesigs (NodeInfo list)
2410         #           .bsig
2411         #           .csig
2412         #           .timestamp
2413         #           .size
2414         #       .bdepends
2415         #       .bdependsigs (NodeInfo list)
2416         #           .bsig
2417         #           .csig
2418         #           .timestamp
2419         #           .size
2420         #       .bimplicit
2421         #       .bimplicitsigs (NodeInfo list)
2422         #           .bsig
2423         #           .csig
2424         #           .timestamp
2425         #           .size
2426         #       .bact
2427         #       .bactsig
2428         #
2429         # The basic idea of the new structure is that a NodeInfo always
2430         # holds all available information about the state of a given Node
2431         # at a certain point in time.  The various .b*sigs lists can just
2432         # be a list of pointers to the .ninfo attributes of the different
2433         # dependent nodes, without any copying of information until it's
2434         # time to pickle it for writing out to a .sconsign file.
2435         #
2436         # The complicating issue is that the *old* format only stored one
2437         # "signature" per dependency, based on however the *last* build
2438         # was configured.  We don't know from just looking at it whether
2439         # it was a build signature, a content signature, or a timestamp
2440         # "signature".  Since we no longer use build signatures, the
2441         # best we can do is look at the length and if it's thirty two,
2442         # assume that it was (or might have been) a content signature.
2443         # If it was actually a build signature, then it will cause a
2444         # rebuild anyway when it doesn't match the new content signature,
2445         # but that's probably the best we can do.
2446         import SCons.SConsign
2447         new_entry = SCons.SConsign.SConsignEntry()
2448         new_entry.binfo = self.new_binfo()
2449         binfo = new_entry.binfo
2450         for attr in self.convert_copy_attrs:
2451             try:
2452                 value = getattr(old_entry, attr)
2453             except AttributeError:
2454                 continue
2455             setattr(binfo, attr, value)
2456             delattr(old_entry, attr)
2457         for attr in self.convert_sig_attrs:
2458             try:
2459                 sig_list = getattr(old_entry, attr)
2460             except AttributeError:
2461                 continue
2462             value = []
2463             for sig in sig_list:
2464                 ninfo = self.new_ninfo()
2465                 if len(sig) == 32:
2466                     ninfo.csig = sig
2467                 else:
2468                     ninfo.timestamp = sig
2469                 value.append(ninfo)
2470             setattr(binfo, attr, value)
2471             delattr(old_entry, attr)
2472         return new_entry
2473
2474     memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
2475
2476     def get_stored_info(self):
2477         try:
2478             return self._memo['get_stored_info']
2479         except KeyError:
2480             pass
2481
2482         try:
2483             sconsign_entry = self.dir.sconsign().get_entry(self.name)
2484         except (KeyError, EnvironmentError):
2485             import SCons.SConsign
2486             sconsign_entry = SCons.SConsign.SConsignEntry()
2487             sconsign_entry.binfo = self.new_binfo()
2488             sconsign_entry.ninfo = self.new_ninfo()
2489         else:
2490             if isinstance(sconsign_entry, FileBuildInfo):
2491                 # This is a .sconsign file from before the Big Signature
2492                 # Refactoring; convert it as best we can.
2493                 sconsign_entry = self.convert_old_entry(sconsign_entry)
2494             try:
2495                 delattr(sconsign_entry.ninfo, 'bsig')
2496             except AttributeError:
2497                 pass
2498
2499         self._memo['get_stored_info'] = sconsign_entry
2500
2501         return sconsign_entry
2502
2503     def get_stored_implicit(self):
2504         binfo = self.get_stored_info().binfo
2505         binfo.prepare_dependencies()
2506         try: return binfo.bimplicit
2507         except AttributeError: return None
2508
2509     def rel_path(self, other):
2510         return self.dir.rel_path(other)
2511
2512     def _get_found_includes_key(self, env, scanner, path):
2513         return (id(env), id(scanner), path)
2514
2515     memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
2516
2517     def get_found_includes(self, env, scanner, path):
2518         """Return the included implicit dependencies in this file.
2519         Cache results so we only scan the file once per path
2520         regardless of how many times this information is requested.
2521         """
2522         memo_key = (id(env), id(scanner), path)
2523         try:
2524             memo_dict = self._memo['get_found_includes']
2525         except KeyError:
2526             memo_dict = {}
2527             self._memo['get_found_includes'] = memo_dict
2528         else:
2529             try:
2530                 return memo_dict[memo_key]
2531             except KeyError:
2532                 pass
2533
2534         if scanner:
2535             # result = [n.disambiguate() for n in scanner(self, env, path)]
2536             result = scanner(self, env, path)
2537             result = map(lambda N: N.disambiguate(), result)
2538         else:
2539             result = []
2540
2541         memo_dict[memo_key] = result
2542
2543         return result
2544
2545     def _createDir(self):
2546         # ensure that the directories for this node are
2547         # created.
2548         self.dir._create()
2549
2550     def retrieve_from_cache(self):
2551         """Try to retrieve the node's content from a cache
2552
2553         This method is called from multiple threads in a parallel build,
2554         so only do thread safe stuff here. Do thread unsafe stuff in
2555         built().
2556
2557         Returns true iff the node was successfully retrieved.
2558         """
2559         if self.nocache:
2560             return None
2561         if not self.is_derived():
2562             return None
2563         return self.get_build_env().get_CacheDir().retrieve(self)
2564
2565     def built(self):
2566         """
2567         Called just after this node is successfully built.
2568         """
2569         # Push this file out to cache before the superclass Node.built()
2570         # method has a chance to clear the build signature, which it
2571         # will do if this file has a source scanner.
2572         #
2573         # We have to clear the memoized values *before* we push it to
2574         # cache so that the memoization of the self.exists() return
2575         # value doesn't interfere.
2576         self.clear_memoized_values()
2577         if self.exists():
2578             self.get_build_env().get_CacheDir().push(self)
2579         SCons.Node.Node.built(self)
2580
2581     def visited(self):
2582         if self.exists():
2583             self.get_build_env().get_CacheDir().push_if_forced(self)
2584
2585         ninfo = self.get_ninfo()
2586
2587         csig = self.get_max_drift_csig()
2588         if csig:
2589             ninfo.csig = csig
2590
2591         ninfo.timestamp = self.get_timestamp()
2592         ninfo.size      = self.get_size()
2593
2594         if not self.has_builder():
2595             # This is a source file, but it might have been a target file
2596             # in another build that included more of the DAG.  Copy
2597             # any build information that's stored in the .sconsign file
2598             # into our binfo object so it doesn't get lost.
2599             old = self.get_stored_info()
2600             self.get_binfo().__dict__.update(old.binfo.__dict__)
2601
2602         self.store_info()
2603
2604     def find_src_builder(self):
2605         if self.rexists():
2606             return None
2607         scb = self.dir.src_builder()
2608         if scb is _null:
2609             if diskcheck_sccs(self.dir, self.name):
2610                 scb = get_DefaultSCCSBuilder()
2611             elif diskcheck_rcs(self.dir, self.name):
2612                 scb = get_DefaultRCSBuilder()
2613             else:
2614                 scb = None
2615         if scb is not None:
2616             try:
2617                 b = self.builder
2618             except AttributeError:
2619                 b = None
2620             if b is None:
2621                 self.builder_set(scb)
2622         return scb
2623
2624     def has_src_builder(self):
2625         """Return whether this Node has a source builder or not.
2626
2627         If this Node doesn't have an explicit source code builder, this
2628         is where we figure out, on the fly, if there's a transparent
2629         source code builder for it.
2630
2631         Note that if we found a source builder, we also set the
2632         self.builder attribute, so that all of the methods that actually
2633         *build* this file don't have to do anything different.
2634         """
2635         try:
2636             scb = self.sbuilder
2637         except AttributeError:
2638             scb = self.sbuilder = self.find_src_builder()
2639         return scb is not None
2640
2641     def alter_targets(self):
2642         """Return any corresponding targets in a variant directory.
2643         """
2644         if self.is_derived():
2645             return [], None
2646         return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
2647
2648     def _rmv_existing(self):
2649         self.clear_memoized_values()
2650         e = Unlink(self, [], None)
2651         if isinstance(e, SCons.Errors.BuildError):
2652             raise e
2653
2654     #
2655     # Taskmaster interface subsystem
2656     #
2657
2658     def make_ready(self):
2659         self.has_src_builder()
2660         self.get_binfo()
2661
2662     def prepare(self):
2663         """Prepare for this file to be created."""
2664         SCons.Node.Node.prepare(self)
2665
2666         if self.get_state() != SCons.Node.up_to_date:
2667             if self.exists():
2668                 if self.is_derived() and not self.precious:
2669                     self._rmv_existing()
2670             else:
2671                 try:
2672                     self._createDir()
2673                 except SCons.Errors.StopError, drive:
2674                     desc = "No drive `%s' for target `%s'." % (drive, self)
2675                     raise SCons.Errors.StopError, desc
2676
2677     #
2678     #
2679     #
2680
2681     def remove(self):
2682         """Remove this file."""
2683         if self.exists() or self.islink():
2684             self.fs.unlink(self.path)
2685             return 1
2686         return None
2687
2688     def do_duplicate(self, src):
2689         self._createDir()
2690         Unlink(self, None, None)
2691         e = Link(self, src, None)
2692         if isinstance(e, SCons.Errors.BuildError):
2693             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
2694             raise SCons.Errors.StopError, desc
2695         self.linked = 1
2696         # The Link() action may or may not have actually
2697         # created the file, depending on whether the -n
2698         # option was used or not.  Delete the _exists and
2699         # _rexists attributes so they can be reevaluated.
2700         self.clear()
2701
2702     memoizer_counters.append(SCons.Memoize.CountValue('exists'))
2703
2704     def exists(self):
2705         try:
2706             return self._memo['exists']
2707         except KeyError:
2708             pass
2709         # Duplicate from source path if we are set up to do this.
2710         if self.duplicate and not self.is_derived() and not self.linked:
2711             src = self.srcnode()
2712             if src is not self:
2713                 # At this point, src is meant to be copied in a variant directory.
2714                 src = src.rfile()
2715                 if src.abspath != self.abspath:
2716                     if src.exists():
2717                         self.do_duplicate(src)
2718                         # Can't return 1 here because the duplication might
2719                         # not actually occur if the -n option is being used.
2720                     else:
2721                         # The source file does not exist.  Make sure no old
2722                         # copy remains in the variant directory.
2723                         if Base.exists(self) or self.islink():
2724                             self.fs.unlink(self.path)
2725                         # Return None explicitly because the Base.exists() call
2726                         # above will have cached its value if the file existed.
2727                         self._memo['exists'] = None
2728                         return None
2729         result = Base.exists(self)
2730         self._memo['exists'] = result
2731         return result
2732
2733     #
2734     # SIGNATURE SUBSYSTEM
2735     #
2736
2737     def get_max_drift_csig(self):
2738         """
2739         Returns the content signature currently stored for this node
2740         if it's been unmodified longer than the max_drift value, or the
2741         max_drift value is 0.  Returns None otherwise.
2742         """
2743         old = self.get_stored_info()
2744         mtime = self.get_timestamp()
2745
2746         max_drift = self.fs.max_drift
2747         if max_drift > 0:
2748             if (time.time() - mtime) > max_drift:
2749                 try:
2750                     n = old.ninfo
2751                     if n.timestamp and n.csig and n.timestamp == mtime:
2752                         return n.csig
2753                 except AttributeError:
2754                     pass
2755         elif max_drift == 0:
2756             try:
2757                 return old.ninfo.csig
2758             except AttributeError:
2759                 pass
2760
2761         return None
2762
2763     def get_csig(self):
2764         """
2765         Generate a node's content signature, the digested signature
2766         of its content.
2767
2768         node - the node
2769         cache - alternate node to use for the signature cache
2770         returns - the content signature
2771         """
2772         ninfo = self.get_ninfo()
2773         try:
2774             return ninfo.csig
2775         except AttributeError:
2776             pass
2777
2778         csig = self.get_max_drift_csig()
2779         if csig is None:
2780
2781             try:
2782                 if self.get_size() < SCons.Node.FS.File.md5_chunksize:
2783                     contents = self.get_contents()
2784                 else:
2785                     csig = self.get_content_hash()
2786             except IOError:
2787                 # This can happen if there's actually a directory on-disk,
2788                 # which can be the case if they've disabled disk checks,
2789                 # or if an action with a File target actually happens to
2790                 # create a same-named directory by mistake.
2791                 csig = ''
2792             else:
2793                 if not csig:
2794                     csig = SCons.Util.MD5signature(contents)
2795
2796         ninfo.csig = csig
2797
2798         return csig
2799
2800     #
2801     # DECISION SUBSYSTEM
2802     #
2803
2804     def builder_set(self, builder):
2805         SCons.Node.Node.builder_set(self, builder)
2806         self.changed_since_last_build = self.decide_target
2807
2808     def changed_content(self, target, prev_ni):
2809         cur_csig = self.get_csig()
2810         try:
2811             return cur_csig != prev_ni.csig
2812         except AttributeError:
2813             return 1
2814
2815     def changed_state(self, target, prev_ni):
2816         return self.state != SCons.Node.up_to_date
2817
2818     def changed_timestamp_then_content(self, target, prev_ni):
2819         if not self.changed_timestamp_match(target, prev_ni):
2820             try:
2821                 self.get_ninfo().csig = prev_ni.csig
2822             except AttributeError:
2823                 pass
2824             return False
2825         return self.changed_content(target, prev_ni)
2826
2827     def changed_timestamp_newer(self, target, prev_ni):
2828         try:
2829             return self.get_timestamp() > target.get_timestamp()
2830         except AttributeError:
2831             return 1
2832
2833     def changed_timestamp_match(self, target, prev_ni):
2834         try:
2835             return self.get_timestamp() != prev_ni.timestamp
2836         except AttributeError:
2837             return 1
2838
2839     def decide_source(self, target, prev_ni):
2840         return target.get_build_env().decide_source(self, target, prev_ni)
2841
2842     def decide_target(self, target, prev_ni):
2843         return target.get_build_env().decide_target(self, target, prev_ni)
2844
2845     # Initialize this Node's decider function to decide_source() because
2846     # every file is a source file until it has a Builder attached...
2847     changed_since_last_build = decide_source
2848
2849     def is_up_to_date(self):
2850         T = 0
2851         if T: Trace('is_up_to_date(%s):' % self)
2852         if not self.exists():
2853             if T: Trace(' not self.exists():')
2854             # The file doesn't exist locally...
2855             r = self.rfile()
2856             if r != self:
2857                 # ...but there is one in a Repository...
2858                 if not self.changed(r):
2859                     if T: Trace(' changed(%s):' % r)
2860                     # ...and it's even up-to-date...
2861                     if self._local:
2862                         # ...and they'd like a local copy.
2863                         e = LocalCopy(self, r, None)
2864                         if isinstance(e, SCons.Errors.BuildError):
2865                             raise 
2866                         self.store_info()
2867                     if T: Trace(' 1\n')
2868                     return 1
2869             self.changed()
2870             if T: Trace(' None\n')
2871             return None
2872         else:
2873             r = self.changed()
2874             if T: Trace(' self.exists():  %s\n' % r)
2875             return not r
2876
2877     memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
2878
2879     def rfile(self):
2880         try:
2881             return self._memo['rfile']
2882         except KeyError:
2883             pass
2884         result = self
2885         if not self.exists():
2886             norm_name = _my_normcase(self.name)
2887             for dir in self.dir.get_all_rdirs():
2888                 try: node = dir.entries[norm_name]
2889                 except KeyError: node = dir.file_on_disk(self.name)
2890                 if node and node.exists() and \
2891                    (isinstance(node, File) or isinstance(node, Entry) \
2892                     or not node.is_derived()):
2893                         result = node
2894                         # Copy over our local attributes to the repository
2895                         # Node so we identify shared object files in the
2896                         # repository and don't assume they're static.
2897                         #
2898                         # This isn't perfect; the attribute would ideally
2899                         # be attached to the object in the repository in
2900                         # case it was built statically in the repository
2901                         # and we changed it to shared locally, but that's
2902                         # rarely the case and would only occur if you
2903                         # intentionally used the same suffix for both
2904                         # shared and static objects anyway.  So this
2905                         # should work well in practice.
2906                         result.attributes = self.attributes
2907                         break
2908         self._memo['rfile'] = result
2909         return result
2910
2911     def rstr(self):
2912         return str(self.rfile())
2913
2914     def get_cachedir_csig(self):
2915         """
2916         Fetch a Node's content signature for purposes of computing
2917         another Node's cachesig.
2918
2919         This is a wrapper around the normal get_csig() method that handles
2920         the somewhat obscure case of using CacheDir with the -n option.
2921         Any files that don't exist would normally be "built" by fetching
2922         them from the cache, but the normal get_csig() method will try
2923         to open up the local file, which doesn't exist because the -n
2924         option meant we didn't actually pull the file from cachedir.
2925         But since the file *does* actually exist in the cachedir, we
2926         can use its contents for the csig.
2927         """
2928         try:
2929             return self.cachedir_csig
2930         except AttributeError:
2931             pass
2932
2933         cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
2934         if not self.exists() and cachefile and os.path.exists(cachefile):
2935             self.cachedir_csig = SCons.Util.MD5filesignature(cachefile, \
2936                 SCons.Node.FS.File.md5_chunksize * 1024)
2937         else:
2938             self.cachedir_csig = self.get_csig()
2939         return self.cachedir_csig
2940
2941     def get_cachedir_bsig(self):
2942         try:
2943             return self.cachesig
2944         except AttributeError:
2945             pass
2946
2947         # Add the path to the cache signature, because multiple
2948         # targets built by the same action will all have the same
2949         # build signature, and we have to differentiate them somehow.
2950         children = self.children()
2951         executor = self.get_executor()
2952         # sigs = [n.get_cachedir_csig() for n in children]
2953         sigs = map(lambda n: n.get_cachedir_csig(), children)
2954         sigs.append(SCons.Util.MD5signature(executor.get_contents()))
2955         sigs.append(self.path)
2956         result = self.cachesig = SCons.Util.MD5collect(sigs)
2957         return result
2958
2959
2960 default_fs = None
2961
2962 def get_default_fs():
2963     global default_fs
2964     if not default_fs:
2965         default_fs = FS()
2966     return default_fs
2967
2968 class FileFinder:
2969     """
2970     """
2971     if SCons.Memoize.use_memoizer:
2972         __metaclass__ = SCons.Memoize.Memoized_Metaclass
2973
2974     memoizer_counters = []
2975
2976     def __init__(self):
2977         self._memo = {}
2978
2979     def filedir_lookup(self, p, fd=None):
2980         """
2981         A helper method for find_file() that looks up a directory for
2982         a file we're trying to find.  This only creates the Dir Node if
2983         it exists on-disk, since if the directory doesn't exist we know
2984         we won't find any files in it...  :-)
2985
2986         It would be more compact to just use this as a nested function
2987         with a default keyword argument (see the commented-out version
2988         below), but that doesn't work unless you have nested scopes,
2989         so we define it here just so this work under Python 1.5.2.
2990         """
2991         if fd is None:
2992             fd = self.default_filedir
2993         dir, name = os.path.split(fd)
2994         drive, d = os.path.splitdrive(dir)
2995         if d in ('/', os.sep):
2996             return p.fs.get_root(drive).dir_on_disk(name)
2997         if dir:
2998             p = self.filedir_lookup(p, dir)
2999             if not p:
3000                 return None
3001         norm_name = _my_normcase(name)
3002         try:
3003             node = p.entries[norm_name]
3004         except KeyError:
3005             return p.dir_on_disk(name)
3006         if isinstance(node, Dir):
3007             return node
3008         if isinstance(node, Entry):
3009             node.must_be_same(Dir)
3010             return node
3011         return None
3012
3013     def _find_file_key(self, filename, paths, verbose=None):
3014         return (filename, paths)
3015         
3016     memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
3017
3018     def find_file(self, filename, paths, verbose=None):
3019         """
3020         find_file(str, [Dir()]) -> [nodes]
3021
3022         filename - a filename to find
3023         paths - a list of directory path *nodes* to search in.  Can be
3024                 represented as a list, a tuple, or a callable that is
3025                 called with no arguments and returns the list or tuple.
3026
3027         returns - the node created from the found file.
3028
3029         Find a node corresponding to either a derived file or a file
3030         that exists already.
3031
3032         Only the first file found is returned, and none is returned
3033         if no file is found.
3034         """
3035         memo_key = self._find_file_key(filename, paths)
3036         try:
3037             memo_dict = self._memo['find_file']
3038         except KeyError:
3039             memo_dict = {}
3040             self._memo['find_file'] = memo_dict
3041         else:
3042             try:
3043                 return memo_dict[memo_key]
3044             except KeyError:
3045                 pass
3046
3047         if verbose and not callable(verbose):
3048             if not SCons.Util.is_String(verbose):
3049                 verbose = "find_file"
3050             verbose = '  %s: ' % verbose
3051             verbose = lambda s, v=verbose: sys.stdout.write(v + s)
3052
3053         filedir, filename = os.path.split(filename)
3054         if filedir:
3055             # More compact code that we can't use until we drop
3056             # support for Python 1.5.2:
3057             #
3058             #def filedir_lookup(p, fd=filedir):
3059             #    """
3060             #    A helper function that looks up a directory for a file
3061             #    we're trying to find.  This only creates the Dir Node
3062             #    if it exists on-disk, since if the directory doesn't
3063             #    exist we know we won't find any files in it...  :-)
3064             #    """
3065             #    dir, name = os.path.split(fd)
3066             #    if dir:
3067             #        p = filedir_lookup(p, dir)
3068             #        if not p:
3069             #            return None
3070             #    norm_name = _my_normcase(name)
3071             #    try:
3072             #        node = p.entries[norm_name]
3073             #    except KeyError:
3074             #        return p.dir_on_disk(name)
3075             #    if isinstance(node, Dir):
3076             #        return node
3077             #    if isinstance(node, Entry):
3078             #        node.must_be_same(Dir)
3079             #        return node
3080             #    if isinstance(node, Dir) or isinstance(node, Entry):
3081             #        return node
3082             #    return None
3083             #paths = filter(None, map(filedir_lookup, paths))
3084
3085             self.default_filedir = filedir
3086             paths = filter(None, map(self.filedir_lookup, paths))
3087
3088         result = None
3089         for dir in paths:
3090             if verbose:
3091                 verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
3092             node, d = dir.srcdir_find_file(filename)
3093             if node:
3094                 if verbose:
3095                     verbose("... FOUND '%s' in '%s'\n" % (filename, d))
3096                 result = node
3097                 break
3098
3099         memo_dict[memo_key] = result
3100
3101         return result
3102
3103 find_file = FileFinder().find_file
3104
3105
3106 def invalidate_node_memos(targets):
3107     """
3108     Invalidate the memoized values of all Nodes (files or directories)
3109     that are associated with the given entries. Has been added to
3110     clear the cache of nodes affected by a direct execution of an
3111     action (e.g.  Delete/Copy/Chmod). Existing Node caches become
3112     inconsistent if the action is run through Execute().  The argument
3113     `targets` can be a single Node object or filename, or a sequence
3114     of Nodes/filenames.
3115     """
3116     from traceback import extract_stack
3117
3118     # First check if the cache really needs to be flushed. Only
3119     # actions run in the SConscript with Execute() seem to be
3120     # affected. XXX The way to check if Execute() is in the stacktrace
3121     # is a very dirty hack and should be replaced by a more sensible
3122     # solution.
3123     for f in extract_stack():
3124         if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
3125             break
3126     else:
3127         # Dont have to invalidate, so return
3128         return
3129
3130     if not SCons.Util.is_List(targets):
3131         targets = [targets]
3132     
3133     for entry in targets:
3134         # If the target is a Node object, clear the cache. If it is a
3135         # filename, look up potentially existing Node object first.
3136         try:
3137             entry.clear_memoized_values()
3138         except AttributeError:
3139             # Not a Node object, try to look up Node by filename.  XXX
3140             # This creates Node objects even for those filenames which
3141             # do not correspond to an existing Node object.
3142             node = get_default_fs().Entry(entry)
3143             if node:
3144                 node.clear_memoized_values()                        
3145