f8911b00ed0eedebf43f6c619f3c14fa4865f3fd
[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 # This is for later so we can differentiate between Entry the class and Entry
970 # the method of the FS class.
971 _classEntry = Entry
972
973
974 class LocalFS:
975
976     if SCons.Memoize.use_memoizer:
977         __metaclass__ = SCons.Memoize.Memoized_Metaclass
978
979     # This class implements an abstraction layer for operations involving
980     # a local file system.  Essentially, this wraps any function in
981     # the os, os.path or shutil modules that we use to actually go do
982     # anything with or to the local file system.
983     #
984     # Note that there's a very good chance we'll refactor this part of
985     # the architecture in some way as we really implement the interface(s)
986     # for remote file system Nodes.  For example, the right architecture
987     # might be to have this be a subclass instead of a base class.
988     # Nevertheless, we're using this as a first step in that direction.
989     #
990     # We're not using chdir() yet because the calling subclass method
991     # needs to use os.chdir() directly to avoid recursion.  Will we
992     # really need this one?
993     #def chdir(self, path):
994     #    return os.chdir(path)
995     def chmod(self, path, mode):
996         return os.chmod(path, mode)
997     def copy(self, src, dst):
998         return shutil.copy(src, dst)
999     def copy2(self, src, dst):
1000         return shutil.copy2(src, dst)
1001     def exists(self, path):
1002         return os.path.exists(path)
1003     def getmtime(self, path):
1004         return os.path.getmtime(path)
1005     def getsize(self, path):
1006         return os.path.getsize(path)
1007     def isdir(self, path):
1008         return os.path.isdir(path)
1009     def isfile(self, path):
1010         return os.path.isfile(path)
1011     def link(self, src, dst):
1012         return os.link(src, dst)
1013     def lstat(self, path):
1014         return os.lstat(path)
1015     def listdir(self, path):
1016         return os.listdir(path)
1017     def makedirs(self, path):
1018         return os.makedirs(path)
1019     def mkdir(self, path):
1020         return os.mkdir(path)
1021     def rename(self, old, new):
1022         return os.rename(old, new)
1023     def stat(self, path):
1024         return os.stat(path)
1025     def symlink(self, src, dst):
1026         return os.symlink(src, dst)
1027     def open(self, path):
1028         return open(path)
1029     def unlink(self, path):
1030         return os.unlink(path)
1031
1032     if hasattr(os, 'symlink'):
1033         def islink(self, path):
1034             return os.path.islink(path)
1035     else:
1036         def islink(self, path):
1037             return 0                    # no symlinks
1038
1039     if hasattr(os, 'readlink'):
1040         def readlink(self, file):
1041             return os.readlink(file)
1042     else:
1043         def readlink(self, file):
1044             return ''
1045
1046
1047 #class RemoteFS:
1048 #    # Skeleton for the obvious methods we might need from the
1049 #    # abstraction layer for a remote filesystem.
1050 #    def upload(self, local_src, remote_dst):
1051 #        pass
1052 #    def download(self, remote_src, local_dst):
1053 #        pass
1054
1055
1056 class FS(LocalFS):
1057
1058     memoizer_counters = []
1059
1060     def __init__(self, path = None):
1061         """Initialize the Node.FS subsystem.
1062
1063         The supplied path is the top of the source tree, where we
1064         expect to find the top-level build file.  If no path is
1065         supplied, the current directory is the default.
1066
1067         The path argument must be a valid absolute path.
1068         """
1069         if __debug__: logInstanceCreation(self, 'Node.FS')
1070
1071         self._memo = {}
1072
1073         self.Root = {}
1074         self.SConstruct_dir = None
1075         self.max_drift = default_max_drift
1076
1077         self.Top = None
1078         if path is None:
1079             self.pathTop = os.getcwd()
1080         else:
1081             self.pathTop = path
1082         self.defaultDrive = _my_normcase(os.path.splitdrive(self.pathTop)[0])
1083
1084         self.Top = self.Dir(self.pathTop)
1085         self.Top.path = '.'
1086         self.Top.tpath = '.'
1087         self._cwd = self.Top
1088
1089         DirNodeInfo.fs = self
1090         FileNodeInfo.fs = self
1091     
1092     def set_SConstruct_dir(self, dir):
1093         self.SConstruct_dir = dir
1094
1095     def get_max_drift(self):
1096         return self.max_drift
1097
1098     def set_max_drift(self, max_drift):
1099         self.max_drift = max_drift
1100
1101     def getcwd(self):
1102         return self._cwd
1103
1104     def chdir(self, dir, change_os_dir=0):
1105         """Change the current working directory for lookups.
1106         If change_os_dir is true, we will also change the "real" cwd
1107         to match.
1108         """
1109         curr=self._cwd
1110         try:
1111             if dir is not None:
1112                 self._cwd = dir
1113                 if change_os_dir:
1114                     os.chdir(dir.abspath)
1115         except OSError:
1116             self._cwd = curr
1117             raise
1118
1119     def get_root(self, drive):
1120         """
1121         Returns the root directory for the specified drive, creating
1122         it if necessary.
1123         """
1124         drive = _my_normcase(drive)
1125         try:
1126             return self.Root[drive]
1127         except KeyError:
1128             root = RootDir(drive, self)
1129             self.Root[drive] = root
1130             if not drive:
1131                 self.Root[self.defaultDrive] = root
1132             elif drive == self.defaultDrive:
1133                 self.Root[''] = root
1134             return root
1135
1136     def _lookup(self, p, directory, fsclass, create=1):
1137         """
1138         The generic entry point for Node lookup with user-supplied data.
1139
1140         This translates arbitrary input into a canonical Node.FS object
1141         of the specified fsclass.  The general approach for strings is
1142         to turn it into a fully normalized absolute path and then call
1143         the root directory's lookup_abs() method for the heavy lifting.
1144
1145         If the path name begins with '#', it is unconditionally
1146         interpreted relative to the top-level directory of this FS.  '#'
1147         is treated as a synonym for the top-level SConstruct directory,
1148         much like '~' is treated as a synonym for the user's home
1149         directory in a UNIX shell.  So both '#foo' and '#/foo' refer
1150         to the 'foo' subdirectory underneath the top-level SConstruct
1151         directory.
1152
1153         If the path name is relative, then the path is looked up relative
1154         to the specified directory, or the current directory (self._cwd,
1155         typically the SConscript directory) if the specified directory
1156         is None.
1157         """
1158         if isinstance(p, Base):
1159             # It's already a Node.FS object.  Make sure it's the right
1160             # class and return.
1161             p.must_be_same(fsclass)
1162             return p
1163         # str(p) in case it's something like a proxy object
1164         p = str(p)
1165
1166         initial_hash = (p[0:1] == '#')
1167         if initial_hash:
1168             # There was an initial '#', so we strip it and override
1169             # whatever directory they may have specified with the
1170             # top-level SConstruct directory.
1171             p = p[1:]
1172             directory = self.Top
1173
1174         if directory and not isinstance(directory, Dir):
1175             directory = self.Dir(directory)
1176
1177         if do_splitdrive:
1178             drive, p = os.path.splitdrive(p)
1179         else:
1180             drive = ''
1181         if drive and not p:
1182             # This causes a naked drive letter to be treated as a synonym
1183             # for the root directory on that drive.
1184             p = os.sep
1185         absolute = os.path.isabs(p)
1186
1187         needs_normpath = needs_normpath_check.match(p)
1188
1189         if initial_hash or not absolute:
1190             # This is a relative lookup, either to the top-level
1191             # SConstruct directory (because of the initial '#') or to
1192             # the current directory (the path name is not absolute).
1193             # Add the string to the appropriate directory lookup path,
1194             # after which the whole thing gets normalized.
1195             if not directory:
1196                 directory = self._cwd
1197             if p:
1198                 p = directory.labspath + '/' + p
1199             else:
1200                 p = directory.labspath
1201
1202         if needs_normpath:
1203             p = os.path.normpath(p)
1204
1205         if drive or absolute:
1206             root = self.get_root(drive)
1207         else:
1208             if not directory:
1209                 directory = self._cwd
1210             root = directory.root
1211
1212         if os.sep != '/':
1213             p = string.replace(p, os.sep, '/')
1214         return root._lookup_abs(p, fsclass, create)
1215
1216     def Entry(self, name, directory = None, create = 1):
1217         """Look up or create a generic Entry node with the specified name.
1218         If the name is a relative path (begins with ./, ../, or a file
1219         name), then it is looked up relative to the supplied directory
1220         node, or to the top level directory of the FS (supplied at
1221         construction time) if no directory is supplied.
1222         """
1223         return self._lookup(name, directory, Entry, create)
1224
1225     def File(self, name, directory = None, create = 1):
1226         """Look up or create a File node with the specified name.  If
1227         the name is a relative path (begins with ./, ../, or a file name),
1228         then it is looked up relative to the supplied directory node,
1229         or to the top level directory of the FS (supplied at construction
1230         time) if no directory is supplied.
1231
1232         This method will raise TypeError if a directory is found at the
1233         specified path.
1234         """
1235         return self._lookup(name, directory, File, create)
1236
1237     def Dir(self, name, directory = None, create = True):
1238         """Look up or create a Dir node with the specified name.  If
1239         the name is a relative path (begins with ./, ../, or a file name),
1240         then it is looked up relative to the supplied directory node,
1241         or to the top level directory of the FS (supplied at construction
1242         time) if no directory is supplied.
1243
1244         This method will raise TypeError if a normal file is found at the
1245         specified path.
1246         """
1247         return self._lookup(name, directory, Dir, create)
1248
1249     def VariantDir(self, variant_dir, src_dir, duplicate=1):
1250         """Link the supplied variant directory to the source directory
1251         for purposes of building files."""
1252
1253         if not isinstance(src_dir, SCons.Node.Node):
1254             src_dir = self.Dir(src_dir)
1255         if not isinstance(variant_dir, SCons.Node.Node):
1256             variant_dir = self.Dir(variant_dir)
1257         if src_dir.is_under(variant_dir):
1258             raise SCons.Errors.UserError, "Source directory cannot be under variant directory."
1259         if variant_dir.srcdir:
1260             if variant_dir.srcdir == src_dir:
1261                 return # We already did this.
1262             raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir)
1263         variant_dir.link(src_dir, duplicate)
1264
1265     def Repository(self, *dirs):
1266         """Specify Repository directories to search."""
1267         for d in dirs:
1268             if not isinstance(d, SCons.Node.Node):
1269                 d = self.Dir(d)
1270             self.Top.addRepository(d)
1271
1272     def variant_dir_target_climb(self, orig, dir, tail):
1273         """Create targets in corresponding variant directories
1274
1275         Climb the directory tree, and look up path names
1276         relative to any linked variant directories we find.
1277
1278         Even though this loops and walks up the tree, we don't memoize
1279         the return value because this is really only used to process
1280         the command-line targets.
1281         """
1282         targets = []
1283         message = None
1284         fmt = "building associated VariantDir targets: %s"
1285         start_dir = dir
1286         while dir:
1287             for bd in dir.variant_dirs:
1288                 if start_dir.is_under(bd):
1289                     # If already in the build-dir location, don't reflect
1290                     return [orig], fmt % str(orig)
1291                 p = apply(os.path.join, [bd.path] + tail)
1292                 targets.append(self.Entry(p))
1293             tail = [dir.name] + tail
1294             dir = dir.up()
1295         if targets:
1296             message = fmt % string.join(map(str, targets))
1297         return targets, message
1298
1299     def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
1300         """
1301         Globs
1302
1303         This is mainly a shim layer 
1304         """
1305         if cwd is None:
1306             cwd = self.getcwd()
1307         return cwd.glob(pathname, ondisk, source, strings)
1308
1309 class DirNodeInfo(SCons.Node.NodeInfoBase):
1310     # This should get reset by the FS initialization.
1311     current_version_id = 1
1312
1313     fs = None
1314
1315     def str_to_node(self, s):
1316         top = self.fs.Top
1317         root = top.root
1318         if do_splitdrive:
1319             drive, s = os.path.splitdrive(s)
1320             if drive:
1321                 root = self.fs.get_root(drive)
1322         if not os.path.isabs(s):
1323             s = top.labspath + '/' + s
1324         return root._lookup_abs(s, Entry)
1325
1326 class DirBuildInfo(SCons.Node.BuildInfoBase):
1327     current_version_id = 1
1328
1329 glob_magic_check = re.compile('[*?[]')
1330
1331 def has_glob_magic(s):
1332     return glob_magic_check.search(s) is not None
1333
1334 class Dir(Base):
1335     """A class for directories in a file system.
1336     """
1337
1338     memoizer_counters = []
1339
1340     NodeInfo = DirNodeInfo
1341     BuildInfo = DirBuildInfo
1342
1343     def __init__(self, name, directory, fs):
1344         if __debug__: logInstanceCreation(self, 'Node.FS.Dir')
1345         Base.__init__(self, name, directory, fs)
1346         self._morph()
1347
1348     def _morph(self):
1349         """Turn a file system Node (either a freshly initialized directory
1350         object or a separate Entry object) into a proper directory object.
1351
1352         Set up this directory's entries and hook it into the file
1353         system tree.  Specify that directories (this Node) don't use
1354         signatures for calculating whether they're current.
1355         """
1356
1357         self.repositories = []
1358         self.srcdir = None
1359
1360         self.entries = {}
1361         self.entries['.'] = self
1362         self.entries['..'] = self.dir
1363         self.cwd = self
1364         self.searched = 0
1365         self._sconsign = None
1366         self.variant_dirs = []
1367         self.root = self.dir.root
1368
1369         # Don't just reset the executor, replace its action list,
1370         # because it might have some pre-or post-actions that need to
1371         # be preserved.
1372         self.builder = get_MkdirBuilder()
1373         self.get_executor().set_action_list(self.builder.action)
1374
1375     def diskcheck_match(self):
1376         diskcheck_match(self, self.isfile,
1377                         "File %s found where directory expected.")
1378
1379     def __clearRepositoryCache(self, duplicate=None):
1380         """Called when we change the repository(ies) for a directory.
1381         This clears any cached information that is invalidated by changing
1382         the repository."""
1383
1384         for node in self.entries.values():
1385             if node != self.dir:
1386                 if node != self and isinstance(node, Dir):
1387                     node.__clearRepositoryCache(duplicate)
1388                 else:
1389                     node.clear()
1390                     try:
1391                         del node._srcreps
1392                     except AttributeError:
1393                         pass
1394                     if duplicate is not None:
1395                         node.duplicate=duplicate
1396
1397     def __resetDuplicate(self, node):
1398         if node != self:
1399             node.duplicate = node.get_dir().duplicate
1400
1401     def Entry(self, name):
1402         """
1403         Looks up or creates an entry node named 'name' relative to
1404         this directory.
1405         """
1406         return self.fs.Entry(name, self)
1407
1408     def Dir(self, name, create=True):
1409         """
1410         Looks up or creates a directory node named 'name' relative to
1411         this directory.
1412         """
1413         return self.fs.Dir(name, self, create)
1414
1415     def File(self, name):
1416         """
1417         Looks up or creates a file node named 'name' relative to
1418         this directory.
1419         """
1420         return self.fs.File(name, self)
1421
1422     def _lookup_rel(self, name, klass, create=1):
1423         """
1424         Looks up a *normalized* relative path name, relative to this
1425         directory.
1426
1427         This method is intended for use by internal lookups with
1428         already-normalized path data.  For general-purpose lookups,
1429         use the Entry(), Dir() and File() methods above.
1430
1431         This method does *no* input checking and will die or give
1432         incorrect results if it's passed a non-normalized path name (e.g.,
1433         a path containing '..'), an absolute path name, a top-relative
1434         ('#foo') path name, or any kind of object.
1435         """
1436         name = self.entry_labspath(name)
1437         return self.root._lookup_abs(name, klass, create)
1438
1439     def link(self, srcdir, duplicate):
1440         """Set this directory as the variant directory for the
1441         supplied source directory."""
1442         self.srcdir = srcdir
1443         self.duplicate = duplicate
1444         self.__clearRepositoryCache(duplicate)
1445         srcdir.variant_dirs.append(self)
1446
1447     def getRepositories(self):
1448         """Returns a list of repositories for this directory.
1449         """
1450         if self.srcdir and not self.duplicate:
1451             return self.srcdir.get_all_rdirs() + self.repositories
1452         return self.repositories
1453
1454     memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs'))
1455
1456     def get_all_rdirs(self):
1457         try:
1458             return list(self._memo['get_all_rdirs'])
1459         except KeyError:
1460             pass
1461
1462         result = [self]
1463         fname = '.'
1464         dir = self
1465         while dir:
1466             for rep in dir.getRepositories():
1467                 result.append(rep.Dir(fname))
1468             if fname == '.':
1469                 fname = dir.name
1470             else:
1471                 fname = dir.name + os.sep + fname
1472             dir = dir.up()
1473
1474         self._memo['get_all_rdirs'] = list(result)
1475
1476         return result
1477
1478     def addRepository(self, dir):
1479         if dir != self and not dir in self.repositories:
1480             self.repositories.append(dir)
1481             dir.tpath = '.'
1482             self.__clearRepositoryCache()
1483
1484     def up(self):
1485         return self.entries['..']
1486
1487     def _rel_path_key(self, other):
1488         return str(other)
1489
1490     memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
1491
1492     def rel_path(self, other):
1493         """Return a path to "other" relative to this directory.
1494         """
1495
1496         # This complicated and expensive method, which constructs relative
1497         # paths between arbitrary Node.FS objects, is no longer used
1498         # by SCons itself.  It was introduced to store dependency paths
1499         # in .sconsign files relative to the target, but that ended up
1500         # being significantly inefficient.
1501         #
1502         # We're continuing to support the method because some SConstruct
1503         # files out there started using it when it was available, and
1504         # we're all about backwards compatibility..
1505
1506         try:
1507             memo_dict = self._memo['rel_path']
1508         except KeyError:
1509             memo_dict = {}
1510             self._memo['rel_path'] = memo_dict
1511         else:
1512             try:
1513                 return memo_dict[other]
1514             except KeyError:
1515                 pass
1516
1517         if self is other:
1518             result = '.'
1519
1520         elif not other in self.path_elements:
1521             try:
1522                 other_dir = other.get_dir()
1523             except AttributeError:
1524                 result = str(other)
1525             else:
1526                 if other_dir is None:
1527                     result = other.name
1528                 else:
1529                     dir_rel_path = self.rel_path(other_dir)
1530                     if dir_rel_path == '.':
1531                         result = other.name
1532                     else:
1533                         result = dir_rel_path + os.sep + other.name
1534         else:
1535             i = self.path_elements.index(other) + 1
1536
1537             path_elems = ['..'] * (len(self.path_elements) - i) \
1538                          + map(lambda n: n.name, other.path_elements[i:])
1539              
1540             result = string.join(path_elems, os.sep)
1541
1542         memo_dict[other] = result
1543
1544         return result
1545
1546     def get_env_scanner(self, env, kw={}):
1547         import SCons.Defaults
1548         return SCons.Defaults.DirEntryScanner
1549
1550     def get_target_scanner(self):
1551         import SCons.Defaults
1552         return SCons.Defaults.DirEntryScanner
1553
1554     def get_found_includes(self, env, scanner, path):
1555         """Return this directory's implicit dependencies.
1556
1557         We don't bother caching the results because the scan typically
1558         shouldn't be requested more than once (as opposed to scanning
1559         .h file contents, which can be requested as many times as the
1560         files is #included by other files).
1561         """
1562         if not scanner:
1563             return []
1564         # Clear cached info for this Dir.  If we already visited this
1565         # directory on our walk down the tree (because we didn't know at
1566         # that point it was being used as the source for another Node)
1567         # then we may have calculated build signature before realizing
1568         # we had to scan the disk.  Now that we have to, though, we need
1569         # to invalidate the old calculated signature so that any node
1570         # dependent on our directory structure gets one that includes
1571         # info about everything on disk.
1572         self.clear()
1573         return scanner(self, env, path)
1574
1575     #
1576     # Taskmaster interface subsystem
1577     #
1578
1579     def prepare(self):
1580         pass
1581
1582     def build(self, **kw):
1583         """A null "builder" for directories."""
1584         global MkdirBuilder
1585         if self.builder is not MkdirBuilder:
1586             apply(SCons.Node.Node.build, [self,], kw)
1587
1588     #
1589     #
1590     #
1591
1592     def _create(self):
1593         """Create this directory, silently and without worrying about
1594         whether the builder is the default or not."""
1595         listDirs = []
1596         parent = self
1597         while parent:
1598             if parent.exists():
1599                 break
1600             listDirs.append(parent)
1601             parent = parent.up()
1602         else:
1603             raise SCons.Errors.StopError, parent.path
1604         listDirs.reverse()
1605         for dirnode in listDirs:
1606             try:
1607                 # Don't call dirnode.build(), call the base Node method
1608                 # directly because we definitely *must* create this
1609                 # directory.  The dirnode.build() method will suppress
1610                 # the build if it's the default builder.
1611                 SCons.Node.Node.build(dirnode)
1612                 dirnode.get_executor().nullify()
1613                 # The build() action may or may not have actually
1614                 # created the directory, depending on whether the -n
1615                 # option was used or not.  Delete the _exists and
1616                 # _rexists attributes so they can be reevaluated.
1617                 dirnode.clear()
1618             except OSError:
1619                 pass
1620
1621     def multiple_side_effect_has_builder(self):
1622         global MkdirBuilder
1623         return self.builder is not MkdirBuilder and self.has_builder()
1624
1625     def alter_targets(self):
1626         """Return any corresponding targets in a variant directory.
1627         """
1628         return self.fs.variant_dir_target_climb(self, self, [])
1629
1630     def scanner_key(self):
1631         """A directory does not get scanned."""
1632         return None
1633
1634     def get_text_contents(self):
1635         """We already emit things in text, so just return the binary
1636         version."""
1637         return self.get_contents()
1638
1639     def get_contents(self):
1640         """Return content signatures and names of all our children
1641         separated by new-lines. Ensure that the nodes are sorted."""
1642         contents = []
1643         name_cmp = lambda a, b: cmp(a.name, b.name)
1644         sorted_children = self.children()[:]
1645         sorted_children.sort(name_cmp)
1646         for node in sorted_children:
1647             contents.append('%s %s\n' % (node.get_csig(), node.name))
1648         return string.join(contents, '')
1649
1650     def get_csig(self):
1651         """Compute the content signature for Directory nodes. In
1652         general, this is not needed and the content signature is not
1653         stored in the DirNodeInfo. However, if get_contents on a Dir
1654         node is called which has a child directory, the child
1655         directory should return the hash of its contents."""
1656         contents = self.get_contents()
1657         return SCons.Util.MD5signature(contents)
1658
1659     def do_duplicate(self, src):
1660         pass
1661
1662     changed_since_last_build = SCons.Node.Node.state_has_changed
1663
1664     def is_up_to_date(self):
1665         """If any child is not up-to-date, then this directory isn't,
1666         either."""
1667         if self.builder is not MkdirBuilder and not self.exists():
1668             return 0
1669         up_to_date = SCons.Node.up_to_date
1670         for kid in self.children():
1671             if kid.get_state() > up_to_date:
1672                 return 0
1673         return 1
1674
1675     def rdir(self):
1676         if not self.exists():
1677             norm_name = _my_normcase(self.name)
1678             for dir in self.dir.get_all_rdirs():
1679                 try: node = dir.entries[norm_name]
1680                 except KeyError: node = dir.dir_on_disk(self.name)
1681                 if node and node.exists() and \
1682                     (isinstance(dir, Dir) or isinstance(dir, Entry)):
1683                         return node
1684         return self
1685
1686     def sconsign(self):
1687         """Return the .sconsign file info for this directory,
1688         creating it first if necessary."""
1689         if not self._sconsign:
1690             import SCons.SConsign
1691             self._sconsign = SCons.SConsign.ForDirectory(self)
1692         return self._sconsign
1693
1694     def srcnode(self):
1695         """Dir has a special need for srcnode()...if we
1696         have a srcdir attribute set, then that *is* our srcnode."""
1697         if self.srcdir:
1698             return self.srcdir
1699         return Base.srcnode(self)
1700
1701     def get_timestamp(self):
1702         """Return the latest timestamp from among our children"""
1703         stamp = 0
1704         for kid in self.children():
1705             if kid.get_timestamp() > stamp:
1706                 stamp = kid.get_timestamp()
1707         return stamp
1708
1709     def entry_abspath(self, name):
1710         return self.abspath + os.sep + name
1711
1712     def entry_labspath(self, name):
1713         return self.labspath + '/' + name
1714
1715     def entry_path(self, name):
1716         return self.path + os.sep + name
1717
1718     def entry_tpath(self, name):
1719         return self.tpath + os.sep + name
1720
1721     def entry_exists_on_disk(self, name):
1722         try:
1723             d = self.on_disk_entries
1724         except AttributeError:
1725             d = {}
1726             try:
1727                 entries = os.listdir(self.abspath)
1728             except OSError:
1729                 pass
1730             else:
1731                 for entry in map(_my_normcase, entries):
1732                     d[entry] = 1
1733             self.on_disk_entries = d
1734         return d.has_key(_my_normcase(name))
1735
1736     memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
1737
1738     def srcdir_list(self):
1739         try:
1740             return self._memo['srcdir_list']
1741         except KeyError:
1742             pass
1743
1744         result = []
1745
1746         dirname = '.'
1747         dir = self
1748         while dir:
1749             if dir.srcdir:
1750                 result.append(dir.srcdir.Dir(dirname))
1751             dirname = dir.name + os.sep + dirname
1752             dir = dir.up()
1753
1754         self._memo['srcdir_list'] = result
1755
1756         return result
1757
1758     def srcdir_duplicate(self, name):
1759         for dir in self.srcdir_list():
1760             if self.is_under(dir):
1761                 # We shouldn't source from something in the build path;
1762                 # variant_dir is probably under src_dir, in which case
1763                 # we are reflecting.
1764                 break
1765             if dir.entry_exists_on_disk(name):
1766                 srcnode = dir.Entry(name).disambiguate()
1767                 if self.duplicate:
1768                     node = self.Entry(name).disambiguate()
1769                     node.do_duplicate(srcnode)
1770                     return node
1771                 else:
1772                     return srcnode
1773         return None
1774
1775     def _srcdir_find_file_key(self, filename):
1776         return filename
1777
1778     memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key))
1779
1780     def srcdir_find_file(self, filename):
1781         try:
1782             memo_dict = self._memo['srcdir_find_file']
1783         except KeyError:
1784             memo_dict = {}
1785             self._memo['srcdir_find_file'] = memo_dict
1786         else:
1787             try:
1788                 return memo_dict[filename]
1789             except KeyError:
1790                 pass
1791
1792         def func(node):
1793             if (isinstance(node, File) or isinstance(node, Entry)) and \
1794                (node.is_derived() or node.exists()):
1795                     return node
1796             return None
1797
1798         norm_name = _my_normcase(filename)
1799
1800         for rdir in self.get_all_rdirs():
1801             try: node = rdir.entries[norm_name]
1802             except KeyError: node = rdir.file_on_disk(filename)
1803             else: node = func(node)
1804             if node:
1805                 result = (node, self)
1806                 memo_dict[filename] = result
1807                 return result
1808
1809         for srcdir in self.srcdir_list():
1810             for rdir in srcdir.get_all_rdirs():
1811                 try: node = rdir.entries[norm_name]
1812                 except KeyError: node = rdir.file_on_disk(filename)
1813                 else: node = func(node)
1814                 if node:
1815                     result = (File(filename, self, self.fs), srcdir)
1816                     memo_dict[filename] = result
1817                     return result
1818
1819         result = (None, None)
1820         memo_dict[filename] = result
1821         return result
1822
1823     def dir_on_disk(self, name):
1824         if self.entry_exists_on_disk(name):
1825             try: return self.Dir(name)
1826             except TypeError: pass
1827         node = self.srcdir_duplicate(name)
1828         if isinstance(node, File):
1829             return None
1830         return node
1831
1832     def file_on_disk(self, name):
1833         if self.entry_exists_on_disk(name) or \
1834            diskcheck_rcs(self, name) or \
1835            diskcheck_sccs(self, name):
1836             try: return self.File(name)
1837             except TypeError: pass
1838         node = self.srcdir_duplicate(name)
1839         if isinstance(node, Dir):
1840             return None
1841         return node
1842
1843     def walk(self, func, arg):
1844         """
1845         Walk this directory tree by calling the specified function
1846         for each directory in the tree.
1847
1848         This behaves like the os.path.walk() function, but for in-memory
1849         Node.FS.Dir objects.  The function takes the same arguments as
1850         the functions passed to os.path.walk():
1851
1852                 func(arg, dirname, fnames)
1853
1854         Except that "dirname" will actually be the directory *Node*,
1855         not the string.  The '.' and '..' entries are excluded from
1856         fnames.  The fnames list may be modified in-place to filter the
1857         subdirectories visited or otherwise impose a specific order.
1858         The "arg" argument is always passed to func() and may be used
1859         in any way (or ignored, passing None is common).
1860         """
1861         entries = self.entries
1862         names = entries.keys()
1863         names.remove('.')
1864         names.remove('..')
1865         func(arg, self, names)
1866         select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
1867         for dirname in filter(select_dirs, names):
1868             entries[dirname].walk(func, arg)
1869
1870     def glob(self, pathname, ondisk=True, source=False, strings=False):
1871         """
1872         Returns a list of Nodes (or strings) matching a specified
1873         pathname pattern.
1874
1875         Pathname patterns follow UNIX shell semantics:  * matches
1876         any-length strings of any characters, ? matches any character,
1877         and [] can enclose lists or ranges of characters.  Matches do
1878         not span directory separators.
1879
1880         The matches take into account Repositories, returning local
1881         Nodes if a corresponding entry exists in a Repository (either
1882         an in-memory Node or something on disk).
1883
1884         By defafult, the glob() function matches entries that exist
1885         on-disk, in addition to in-memory Nodes.  Setting the "ondisk"
1886         argument to False (or some other non-true value) causes the glob()
1887         function to only match in-memory Nodes.  The default behavior is
1888         to return both the on-disk and in-memory Nodes.
1889
1890         The "source" argument, when true, specifies that corresponding
1891         source Nodes must be returned if you're globbing in a build
1892         directory (initialized with VariantDir()).  The default behavior
1893         is to return Nodes local to the VariantDir().
1894
1895         The "strings" argument, when true, returns the matches as strings,
1896         not Nodes.  The strings are path names relative to this directory.
1897
1898         The underlying algorithm is adapted from the glob.glob() function
1899         in the Python library (but heavily modified), and uses fnmatch()
1900         under the covers.
1901         """
1902         dirname, basename = os.path.split(pathname)
1903         if not dirname:
1904             return self._glob1(basename, ondisk, source, strings)
1905         if has_glob_magic(dirname):
1906             list = self.glob(dirname, ondisk, source, strings=False)
1907         else:
1908             list = [self.Dir(dirname, create=True)]
1909         result = []
1910         for dir in list:
1911             r = dir._glob1(basename, ondisk, source, strings)
1912             if strings:
1913                 r = map(lambda x, d=str(dir): os.path.join(d, x), r)
1914             result.extend(r)
1915         result.sort(lambda a, b: cmp(str(a), str(b)))
1916         return result
1917
1918     def _glob1(self, pattern, ondisk=True, source=False, strings=False):
1919         """
1920         Globs for and returns a list of entry names matching a single
1921         pattern in this directory.
1922
1923         This searches any repositories and source directories for
1924         corresponding entries and returns a Node (or string) relative
1925         to the current directory if an entry is found anywhere.
1926
1927         TODO: handle pattern with no wildcard
1928         """
1929         search_dir_list = self.get_all_rdirs()
1930         for srcdir in self.srcdir_list():
1931             search_dir_list.extend(srcdir.get_all_rdirs())
1932
1933         selfEntry = self.Entry
1934         names = []
1935         for dir in search_dir_list:
1936             # We use the .name attribute from the Node because the keys of
1937             # the dir.entries dictionary are normalized (that is, all upper
1938             # case) on case-insensitive systems like Windows.
1939             #node_names = [ v.name for k, v in dir.entries.items() if k not in ('.', '..') ]
1940             entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys())
1941             node_names = map(lambda n, e=dir.entries: e[n].name, entry_names)
1942             names.extend(node_names)
1943             if not strings:
1944                 # Make sure the working directory (self) actually has
1945                 # entries for all Nodes in repositories or variant dirs.
1946                 map(selfEntry, node_names)
1947             if ondisk:
1948                 try:
1949                     disk_names = os.listdir(dir.abspath)
1950                 except os.error:
1951                     continue
1952                 names.extend(disk_names)
1953                 if not strings:
1954                     # We're going to return corresponding Nodes in
1955                     # the local directory, so we need to make sure
1956                     # those Nodes exist.  We only want to create
1957                     # Nodes for the entries that will match the
1958                     # specified pattern, though, which means we
1959                     # need to filter the list here, even though
1960                     # the overall list will also be filtered later,
1961                     # after we exit this loop.
1962                     if pattern[0] != '.':
1963                         #disk_names = [ d for d in disk_names if d[0] != '.' ]
1964                         disk_names = filter(lambda x: x[0] != '.', disk_names)
1965                     disk_names = fnmatch.filter(disk_names, pattern)
1966                     dirEntry = dir.Entry
1967                     for name in disk_names:
1968                         # Add './' before disk filename so that '#' at
1969                         # beginning of filename isn't interpreted.
1970                         name = './' + name
1971                         node = dirEntry(name).disambiguate()
1972                         n = selfEntry(name)
1973                         if n.__class__ != node.__class__:
1974                             n.__class__ = node.__class__
1975                             n._morph()
1976
1977         names = set(names)
1978         if pattern[0] != '.':
1979             #names = [ n for n in names if n[0] != '.' ]
1980             names = filter(lambda x: x[0] != '.', names)
1981         names = fnmatch.filter(names, pattern)
1982
1983         if strings:
1984             return names
1985
1986         #return [ self.entries[_my_normcase(n)] for n in names ]
1987         return map(lambda n, e=self.entries:  e[_my_normcase(n)], names)
1988
1989 class RootDir(Dir):
1990     """A class for the root directory of a file system.
1991
1992     This is the same as a Dir class, except that the path separator
1993     ('/' or '\\') is actually part of the name, so we don't need to
1994     add a separator when creating the path names of entries within
1995     this directory.
1996     """
1997     def __init__(self, name, fs):
1998         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
1999         # We're going to be our own parent directory (".." entry and .dir
2000         # attribute) so we have to set up some values so Base.__init__()
2001         # won't gag won't it calls some of our methods.
2002         self.abspath = ''
2003         self.labspath = ''
2004         self.path = ''
2005         self.tpath = ''
2006         self.path_elements = []
2007         self.duplicate = 0
2008         self.root = self
2009         Base.__init__(self, name, self, fs)
2010
2011         # Now set our paths to what we really want them to be: the
2012         # initial drive letter (the name) plus the directory separator,
2013         # except for the "lookup abspath," which does not have the
2014         # drive letter.
2015         self.abspath = name + os.sep
2016         self.labspath = ''
2017         self.path = name + os.sep
2018         self.tpath = name + os.sep
2019         self._morph()
2020
2021         self._lookupDict = {}
2022
2023         # The // and os.sep + os.sep entries are necessary because
2024         # os.path.normpath() seems to preserve double slashes at the
2025         # beginning of a path (presumably for UNC path names), but
2026         # collapses triple slashes to a single slash.
2027         self._lookupDict[''] = self
2028         self._lookupDict['/'] = self
2029         self._lookupDict['//'] = self
2030         self._lookupDict[os.sep] = self
2031         self._lookupDict[os.sep + os.sep] = self
2032
2033     def must_be_same(self, klass):
2034         if klass is Dir:
2035             return
2036         Base.must_be_same(self, klass)
2037
2038     def _lookup_abs(self, p, klass, create=1):
2039         """
2040         Fast (?) lookup of a *normalized* absolute path.
2041
2042         This method is intended for use by internal lookups with
2043         already-normalized path data.  For general-purpose lookups,
2044         use the FS.Entry(), FS.Dir() or FS.File() methods.
2045
2046         The caller is responsible for making sure we're passed a
2047         normalized absolute path; we merely let Python's dictionary look
2048         up and return the One True Node.FS object for the path.
2049
2050         If no Node for the specified "p" doesn't already exist, and
2051         "create" is specified, the Node may be created after recursive
2052         invocation to find or create the parent directory or directories.
2053         """
2054         k = _my_normcase(p)
2055         try:
2056             result = self._lookupDict[k]
2057         except KeyError:
2058             if not create:
2059                 raise SCons.Errors.UserError
2060             # There is no Node for this path name, and we're allowed
2061             # to create it.
2062             dir_name, file_name = os.path.split(p)
2063             dir_node = self._lookup_abs(dir_name, Dir)
2064             result = klass(file_name, dir_node, self.fs)
2065
2066             # Double-check on disk (as configured) that the Node we
2067             # created matches whatever is out there in the real world.
2068             result.diskcheck_match()
2069
2070             self._lookupDict[k] = result
2071             dir_node.entries[_my_normcase(file_name)] = result
2072             dir_node.implicit = None
2073         else:
2074             # There is already a Node for this path name.  Allow it to
2075             # complain if we were looking for an inappropriate type.
2076             result.must_be_same(klass)
2077         return result
2078
2079     def __str__(self):
2080         return self.abspath
2081
2082     def entry_abspath(self, name):
2083         return self.abspath + name
2084
2085     def entry_labspath(self, name):
2086         return '/' + name
2087
2088     def entry_path(self, name):
2089         return self.path + name
2090
2091     def entry_tpath(self, name):
2092         return self.tpath + name
2093
2094     def is_under(self, dir):
2095         if self is dir:
2096             return 1
2097         else:
2098             return 0
2099
2100     def up(self):
2101         return None
2102
2103     def get_dir(self):
2104         return None
2105
2106     def src_builder(self):
2107         return _null
2108
2109 class FileNodeInfo(SCons.Node.NodeInfoBase):
2110     current_version_id = 1
2111
2112     field_list = ['csig', 'timestamp', 'size']
2113
2114     # This should get reset by the FS initialization.
2115     fs = None
2116
2117     def str_to_node(self, s):
2118         top = self.fs.Top
2119         root = top.root
2120         if do_splitdrive:
2121             drive, s = os.path.splitdrive(s)
2122             if drive:
2123                 root = self.fs.get_root(drive)
2124         if not os.path.isabs(s):
2125             s = top.labspath + '/' + s
2126         return root._lookup_abs(s, Entry)
2127
2128 class FileBuildInfo(SCons.Node.BuildInfoBase):
2129     current_version_id = 1
2130
2131     def convert_to_sconsign(self):
2132         """
2133         Converts this FileBuildInfo object for writing to a .sconsign file
2134
2135         This replaces each Node in our various dependency lists with its
2136         usual string representation: relative to the top-level SConstruct
2137         directory, or an absolute path if it's outside.
2138         """
2139         if os.sep == '/':
2140             node_to_str = str
2141         else:
2142             def node_to_str(n):
2143                 try:
2144                     s = n.path
2145                 except AttributeError:
2146                     s = str(n)
2147                 else:
2148                     s = string.replace(s, os.sep, '/')
2149                 return s
2150         for attr in ['bsources', 'bdepends', 'bimplicit']:
2151             try:
2152                 val = getattr(self, attr)
2153             except AttributeError:
2154                 pass
2155             else:
2156                 setattr(self, attr, map(node_to_str, val))
2157     def convert_from_sconsign(self, dir, name):
2158         """
2159         Converts a newly-read FileBuildInfo object for in-SCons use
2160
2161         For normal up-to-date checking, we don't have any conversion to
2162         perform--but we're leaving this method here to make that clear.
2163         """
2164         pass
2165     def prepare_dependencies(self):
2166         """
2167         Prepares a FileBuildInfo object for explaining what changed
2168
2169         The bsources, bdepends and bimplicit lists have all been
2170         stored on disk as paths relative to the top-level SConstruct
2171         directory.  Convert the strings to actual Nodes (for use by the
2172         --debug=explain code and --implicit-cache).
2173         """
2174         attrs = [
2175             ('bsources', 'bsourcesigs'),
2176             ('bdepends', 'bdependsigs'),
2177             ('bimplicit', 'bimplicitsigs'),
2178         ]
2179         for (nattr, sattr) in attrs:
2180             try:
2181                 strings = getattr(self, nattr)
2182                 nodeinfos = getattr(self, sattr)
2183             except AttributeError:
2184                 continue
2185             nodes = []
2186             for s, ni in izip(strings, nodeinfos):
2187                 if not isinstance(s, SCons.Node.Node):
2188                     s = ni.str_to_node(s)
2189                 nodes.append(s)
2190             setattr(self, nattr, nodes)
2191     def format(self, names=0):
2192         result = []
2193         bkids = self.bsources + self.bdepends + self.bimplicit
2194         bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
2195         for bkid, bkidsig in izip(bkids, bkidsigs):
2196             result.append(str(bkid) + ': ' +
2197                           string.join(bkidsig.format(names=names), ' '))
2198         result.append('%s [%s]' % (self.bactsig, self.bact))
2199         return string.join(result, '\n')
2200
2201 class File(Base):
2202     """A class for files in a file system.
2203     """
2204
2205     memoizer_counters = []
2206
2207     NodeInfo = FileNodeInfo
2208     BuildInfo = FileBuildInfo
2209
2210     md5_chunksize = 64
2211
2212     def diskcheck_match(self):
2213         diskcheck_match(self, self.isdir,
2214                         "Directory %s found where file expected.")
2215
2216     def __init__(self, name, directory, fs):
2217         if __debug__: logInstanceCreation(self, 'Node.FS.File')
2218         Base.__init__(self, name, directory, fs)
2219         self._morph()
2220
2221     def Entry(self, name):
2222         """Create an entry node named 'name' relative to
2223         the directory of this file."""
2224         return self.dir.Entry(name)
2225
2226     def Dir(self, name, create=True):
2227         """Create a directory node named 'name' relative to
2228         the directory of this file."""
2229         return self.dir.Dir(name, create=create)
2230
2231     def Dirs(self, pathlist):
2232         """Create a list of directories relative to the SConscript
2233         directory of this file."""
2234         # TODO(1.5)
2235         # return [self.Dir(p) for p in pathlist]
2236         return map(lambda p, s=self: s.Dir(p), pathlist)
2237
2238     def File(self, name):
2239         """Create a file node named 'name' relative to
2240         the directory of this file."""
2241         return self.dir.File(name)
2242
2243     #def generate_build_dict(self):
2244     #    """Return an appropriate dictionary of values for building
2245     #    this File."""
2246     #    return {'Dir' : self.Dir,
2247     #            'File' : self.File,
2248     #            'RDirs' : self.RDirs}
2249
2250     def _morph(self):
2251         """Turn a file system node into a File object."""
2252         self.scanner_paths = {}
2253         if not hasattr(self, '_local'):
2254             self._local = 0
2255
2256         # If there was already a Builder set on this entry, then
2257         # we need to make sure we call the target-decider function,
2258         # not the source-decider.  Reaching in and doing this by hand
2259         # is a little bogus.  We'd prefer to handle this by adding
2260         # an Entry.builder_set() method that disambiguates like the
2261         # other methods, but that starts running into problems with the
2262         # fragile way we initialize Dir Nodes with their Mkdir builders,
2263         # yet still allow them to be overridden by the user.  Since it's
2264         # not clear right now how to fix that, stick with what works
2265         # until it becomes clear...
2266         if self.has_builder():
2267             self.changed_since_last_build = self.decide_target
2268
2269     def scanner_key(self):
2270         return self.get_suffix()
2271
2272     def get_contents(self):
2273         if not self.rexists():
2274             return ''
2275         fname = self.rfile().abspath
2276         try:
2277             contents = open(fname, "rb").read()
2278         except EnvironmentError, e:
2279             if not e.filename:
2280                 e.filename = fname
2281             raise
2282         return contents
2283
2284     try:
2285         import codecs
2286     except ImportError:
2287         get_text_contents = get_contents
2288     else:
2289         # This attempts to figure out what the encoding of the text is
2290         # based upon the BOM bytes, and then decodes the contents so that
2291         # it's a valid python string.
2292         def get_text_contents(self):
2293             contents = self.get_contents()
2294             if contents.startswith(codecs.BOM_UTF8):
2295                 contents = contents.decode('utf-8')
2296             elif contents.startswith(codecs.BOM_UTF16):
2297                 contents = contents.decode('utf-16')
2298             return contents
2299
2300     def get_content_hash(self):
2301         """
2302         Compute and return the MD5 hash for this file.
2303         """
2304         if not self.rexists():
2305             return SCons.Util.MD5signature('')
2306         fname = self.rfile().abspath
2307         try:
2308             cs = SCons.Util.MD5filesignature(fname,
2309                 chunksize=SCons.Node.FS.File.md5_chunksize*1024)
2310         except EnvironmentError, e:
2311             if not e.filename:
2312                 e.filename = fname
2313             raise
2314         return cs
2315         
2316
2317     memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
2318
2319     def get_size(self):
2320         try:
2321             return self._memo['get_size']
2322         except KeyError:
2323             pass
2324
2325         if self.rexists():
2326             size = self.rfile().getsize()
2327         else:
2328             size = 0
2329
2330         self._memo['get_size'] = size
2331
2332         return size
2333
2334     memoizer_counters.append(SCons.Memoize.CountValue('get_timestamp'))
2335
2336     def get_timestamp(self):
2337         try:
2338             return self._memo['get_timestamp']
2339         except KeyError:
2340             pass
2341
2342         if self.rexists():
2343             timestamp = self.rfile().getmtime()
2344         else:
2345             timestamp = 0
2346
2347         self._memo['get_timestamp'] = timestamp
2348
2349         return timestamp
2350
2351     def store_info(self):
2352         # Merge our build information into the already-stored entry.
2353         # This accomodates "chained builds" where a file that's a target
2354         # in one build (SConstruct file) is a source in a different build.
2355         # See test/chained-build.py for the use case.
2356         if do_store_info:
2357             self.dir.sconsign().store_info(self.name, self)
2358
2359     convert_copy_attrs = [
2360         'bsources',
2361         'bimplicit',
2362         'bdepends',
2363         'bact',
2364         'bactsig',
2365         'ninfo',
2366     ]
2367
2368
2369     convert_sig_attrs = [
2370         'bsourcesigs',
2371         'bimplicitsigs',
2372         'bdependsigs',
2373     ]
2374
2375     def convert_old_entry(self, old_entry):
2376         # Convert a .sconsign entry from before the Big Signature
2377         # Refactoring, doing what we can to convert its information
2378         # to the new .sconsign entry format.
2379         #
2380         # The old format looked essentially like this:
2381         #
2382         #   BuildInfo
2383         #       .ninfo (NodeInfo)
2384         #           .bsig
2385         #           .csig
2386         #           .timestamp
2387         #           .size
2388         #       .bsources
2389         #       .bsourcesigs ("signature" list)
2390         #       .bdepends
2391         #       .bdependsigs ("signature" list)
2392         #       .bimplicit
2393         #       .bimplicitsigs ("signature" list)
2394         #       .bact
2395         #       .bactsig
2396         #
2397         # The new format looks like this:
2398         #
2399         #   .ninfo (NodeInfo)
2400         #       .bsig
2401         #       .csig
2402         #       .timestamp
2403         #       .size
2404         #   .binfo (BuildInfo)
2405         #       .bsources
2406         #       .bsourcesigs (NodeInfo list)
2407         #           .bsig
2408         #           .csig
2409         #           .timestamp
2410         #           .size
2411         #       .bdepends
2412         #       .bdependsigs (NodeInfo list)
2413         #           .bsig
2414         #           .csig
2415         #           .timestamp
2416         #           .size
2417         #       .bimplicit
2418         #       .bimplicitsigs (NodeInfo list)
2419         #           .bsig
2420         #           .csig
2421         #           .timestamp
2422         #           .size
2423         #       .bact
2424         #       .bactsig
2425         #
2426         # The basic idea of the new structure is that a NodeInfo always
2427         # holds all available information about the state of a given Node
2428         # at a certain point in time.  The various .b*sigs lists can just
2429         # be a list of pointers to the .ninfo attributes of the different
2430         # dependent nodes, without any copying of information until it's
2431         # time to pickle it for writing out to a .sconsign file.
2432         #
2433         # The complicating issue is that the *old* format only stored one
2434         # "signature" per dependency, based on however the *last* build
2435         # was configured.  We don't know from just looking at it whether
2436         # it was a build signature, a content signature, or a timestamp
2437         # "signature".  Since we no longer use build signatures, the
2438         # best we can do is look at the length and if it's thirty two,
2439         # assume that it was (or might have been) a content signature.
2440         # If it was actually a build signature, then it will cause a
2441         # rebuild anyway when it doesn't match the new content signature,
2442         # but that's probably the best we can do.
2443         import SCons.SConsign
2444         new_entry = SCons.SConsign.SConsignEntry()
2445         new_entry.binfo = self.new_binfo()
2446         binfo = new_entry.binfo
2447         for attr in self.convert_copy_attrs:
2448             try:
2449                 value = getattr(old_entry, attr)
2450             except AttributeError:
2451                 continue
2452             setattr(binfo, attr, value)
2453             delattr(old_entry, attr)
2454         for attr in self.convert_sig_attrs:
2455             try:
2456                 sig_list = getattr(old_entry, attr)
2457             except AttributeError:
2458                 continue
2459             value = []
2460             for sig in sig_list:
2461                 ninfo = self.new_ninfo()
2462                 if len(sig) == 32:
2463                     ninfo.csig = sig
2464                 else:
2465                     ninfo.timestamp = sig
2466                 value.append(ninfo)
2467             setattr(binfo, attr, value)
2468             delattr(old_entry, attr)
2469         return new_entry
2470
2471     memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
2472
2473     def get_stored_info(self):
2474         try:
2475             return self._memo['get_stored_info']
2476         except KeyError:
2477             pass
2478
2479         try:
2480             sconsign_entry = self.dir.sconsign().get_entry(self.name)
2481         except (KeyError, EnvironmentError):
2482             import SCons.SConsign
2483             sconsign_entry = SCons.SConsign.SConsignEntry()
2484             sconsign_entry.binfo = self.new_binfo()
2485             sconsign_entry.ninfo = self.new_ninfo()
2486         else:
2487             if isinstance(sconsign_entry, FileBuildInfo):
2488                 # This is a .sconsign file from before the Big Signature
2489                 # Refactoring; convert it as best we can.
2490                 sconsign_entry = self.convert_old_entry(sconsign_entry)
2491             try:
2492                 delattr(sconsign_entry.ninfo, 'bsig')
2493             except AttributeError:
2494                 pass
2495
2496         self._memo['get_stored_info'] = sconsign_entry
2497
2498         return sconsign_entry
2499
2500     def get_stored_implicit(self):
2501         binfo = self.get_stored_info().binfo
2502         binfo.prepare_dependencies()
2503         try: return binfo.bimplicit
2504         except AttributeError: return None
2505
2506     def rel_path(self, other):
2507         return self.dir.rel_path(other)
2508
2509     def _get_found_includes_key(self, env, scanner, path):
2510         return (id(env), id(scanner), path)
2511
2512     memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key))
2513
2514     def get_found_includes(self, env, scanner, path):
2515         """Return the included implicit dependencies in this file.
2516         Cache results so we only scan the file once per path
2517         regardless of how many times this information is requested.
2518         """
2519         memo_key = (id(env), id(scanner), path)
2520         try:
2521             memo_dict = self._memo['get_found_includes']
2522         except KeyError:
2523             memo_dict = {}
2524             self._memo['get_found_includes'] = memo_dict
2525         else:
2526             try:
2527                 return memo_dict[memo_key]
2528             except KeyError:
2529                 pass
2530
2531         if scanner:
2532             # result = [n.disambiguate() for n in scanner(self, env, path)]
2533             result = scanner(self, env, path)
2534             result = map(lambda N: N.disambiguate(), result)
2535         else:
2536             result = []
2537
2538         memo_dict[memo_key] = result
2539
2540         return result
2541
2542     def _createDir(self):
2543         # ensure that the directories for this node are
2544         # created.
2545         self.dir._create()
2546
2547     def retrieve_from_cache(self):
2548         """Try to retrieve the node's content from a cache
2549
2550         This method is called from multiple threads in a parallel build,
2551         so only do thread safe stuff here. Do thread unsafe stuff in
2552         built().
2553
2554         Returns true iff the node was successfully retrieved.
2555         """
2556         if self.nocache:
2557             return None
2558         if not self.is_derived():
2559             return None
2560         return self.get_build_env().get_CacheDir().retrieve(self)
2561
2562     def built(self):
2563         """
2564         Called just after this node is successfully built.
2565         """
2566         # Push this file out to cache before the superclass Node.built()
2567         # method has a chance to clear the build signature, which it
2568         # will do if this file has a source scanner.
2569         #
2570         # We have to clear the memoized values *before* we push it to
2571         # cache so that the memoization of the self.exists() return
2572         # value doesn't interfere.
2573         self.clear_memoized_values()
2574         if self.exists():
2575             self.get_build_env().get_CacheDir().push(self)
2576         SCons.Node.Node.built(self)
2577
2578     def visited(self):
2579         if self.exists():
2580             self.get_build_env().get_CacheDir().push_if_forced(self)
2581
2582         ninfo = self.get_ninfo()
2583
2584         csig = self.get_max_drift_csig()
2585         if csig:
2586             ninfo.csig = csig
2587
2588         ninfo.timestamp = self.get_timestamp()
2589         ninfo.size      = self.get_size()
2590
2591         if not self.has_builder():
2592             # This is a source file, but it might have been a target file
2593             # in another build that included more of the DAG.  Copy
2594             # any build information that's stored in the .sconsign file
2595             # into our binfo object so it doesn't get lost.
2596             old = self.get_stored_info()
2597             self.get_binfo().__dict__.update(old.binfo.__dict__)
2598
2599         self.store_info()
2600
2601     def find_src_builder(self):
2602         if self.rexists():
2603             return None
2604         scb = self.dir.src_builder()
2605         if scb is _null:
2606             if diskcheck_sccs(self.dir, self.name):
2607                 scb = get_DefaultSCCSBuilder()
2608             elif diskcheck_rcs(self.dir, self.name):
2609                 scb = get_DefaultRCSBuilder()
2610             else:
2611                 scb = None
2612         if scb is not None:
2613             try:
2614                 b = self.builder
2615             except AttributeError:
2616                 b = None
2617             if b is None:
2618                 self.builder_set(scb)
2619         return scb
2620
2621     def has_src_builder(self):
2622         """Return whether this Node has a source builder or not.
2623
2624         If this Node doesn't have an explicit source code builder, this
2625         is where we figure out, on the fly, if there's a transparent
2626         source code builder for it.
2627
2628         Note that if we found a source builder, we also set the
2629         self.builder attribute, so that all of the methods that actually
2630         *build* this file don't have to do anything different.
2631         """
2632         try:
2633             scb = self.sbuilder
2634         except AttributeError:
2635             scb = self.sbuilder = self.find_src_builder()
2636         return scb is not None
2637
2638     def alter_targets(self):
2639         """Return any corresponding targets in a variant directory.
2640         """
2641         if self.is_derived():
2642             return [], None
2643         return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
2644
2645     def _rmv_existing(self):
2646         self.clear_memoized_values()
2647         e = Unlink(self, [], None)
2648         if isinstance(e, SCons.Errors.BuildError):
2649             raise e
2650
2651     #
2652     # Taskmaster interface subsystem
2653     #
2654
2655     def make_ready(self):
2656         self.has_src_builder()
2657         self.get_binfo()
2658
2659     def prepare(self):
2660         """Prepare for this file to be created."""
2661         SCons.Node.Node.prepare(self)
2662
2663         if self.get_state() != SCons.Node.up_to_date:
2664             if self.exists():
2665                 if self.is_derived() and not self.precious:
2666                     self._rmv_existing()
2667             else:
2668                 try:
2669                     self._createDir()
2670                 except SCons.Errors.StopError, drive:
2671                     desc = "No drive `%s' for target `%s'." % (drive, self)
2672                     raise SCons.Errors.StopError, desc
2673
2674     #
2675     #
2676     #
2677
2678     def remove(self):
2679         """Remove this file."""
2680         if self.exists() or self.islink():
2681             self.fs.unlink(self.path)
2682             return 1
2683         return None
2684
2685     def do_duplicate(self, src):
2686         self._createDir()
2687         Unlink(self, None, None)
2688         e = Link(self, src, None)
2689         if isinstance(e, SCons.Errors.BuildError):
2690             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
2691             raise SCons.Errors.StopError, desc
2692         self.linked = 1
2693         # The Link() action may or may not have actually
2694         # created the file, depending on whether the -n
2695         # option was used or not.  Delete the _exists and
2696         # _rexists attributes so they can be reevaluated.
2697         self.clear()
2698
2699     memoizer_counters.append(SCons.Memoize.CountValue('exists'))
2700
2701     def exists(self):
2702         try:
2703             return self._memo['exists']
2704         except KeyError:
2705             pass
2706         # Duplicate from source path if we are set up to do this.
2707         if self.duplicate and not self.is_derived() and not self.linked:
2708             src = self.srcnode()
2709             if src is not self:
2710                 # At this point, src is meant to be copied in a variant directory.
2711                 src = src.rfile()
2712                 if src.abspath != self.abspath:
2713                     if src.exists():
2714                         self.do_duplicate(src)
2715                         # Can't return 1 here because the duplication might
2716                         # not actually occur if the -n option is being used.
2717                     else:
2718                         # The source file does not exist.  Make sure no old
2719                         # copy remains in the variant directory.
2720                         if Base.exists(self) or self.islink():
2721                             self.fs.unlink(self.path)
2722                         # Return None explicitly because the Base.exists() call
2723                         # above will have cached its value if the file existed.
2724                         self._memo['exists'] = None
2725                         return None
2726         result = Base.exists(self)
2727         self._memo['exists'] = result
2728         return result
2729
2730     #
2731     # SIGNATURE SUBSYSTEM
2732     #
2733
2734     def get_max_drift_csig(self):
2735         """
2736         Returns the content signature currently stored for this node
2737         if it's been unmodified longer than the max_drift value, or the
2738         max_drift value is 0.  Returns None otherwise.
2739         """
2740         old = self.get_stored_info()
2741         mtime = self.get_timestamp()
2742
2743         max_drift = self.fs.max_drift
2744         if max_drift > 0:
2745             if (time.time() - mtime) > max_drift:
2746                 try:
2747                     n = old.ninfo
2748                     if n.timestamp and n.csig and n.timestamp == mtime:
2749                         return n.csig
2750                 except AttributeError:
2751                     pass
2752         elif max_drift == 0:
2753             try:
2754                 return old.ninfo.csig
2755             except AttributeError:
2756                 pass
2757
2758         return None
2759
2760     def get_csig(self):
2761         """
2762         Generate a node's content signature, the digested signature
2763         of its content.
2764
2765         node - the node
2766         cache - alternate node to use for the signature cache
2767         returns - the content signature
2768         """
2769         ninfo = self.get_ninfo()
2770         try:
2771             return ninfo.csig
2772         except AttributeError:
2773             pass
2774
2775         csig = self.get_max_drift_csig()
2776         if csig is None:
2777
2778             try:
2779                 if self.get_size() < SCons.Node.FS.File.md5_chunksize:
2780                     contents = self.get_contents()
2781                 else:
2782                     csig = self.get_content_hash()
2783             except IOError:
2784                 # This can happen if there's actually a directory on-disk,
2785                 # which can be the case if they've disabled disk checks,
2786                 # or if an action with a File target actually happens to
2787                 # create a same-named directory by mistake.
2788                 csig = ''
2789             else:
2790                 if not csig:
2791                     csig = SCons.Util.MD5signature(contents)
2792
2793         ninfo.csig = csig
2794
2795         return csig
2796
2797     #
2798     # DECISION SUBSYSTEM
2799     #
2800
2801     def builder_set(self, builder):
2802         SCons.Node.Node.builder_set(self, builder)
2803         self.changed_since_last_build = self.decide_target
2804
2805     def changed_content(self, target, prev_ni):
2806         cur_csig = self.get_csig()
2807         try:
2808             return cur_csig != prev_ni.csig
2809         except AttributeError:
2810             return 1
2811
2812     def changed_state(self, target, prev_ni):
2813         return self.state != SCons.Node.up_to_date
2814
2815     def changed_timestamp_then_content(self, target, prev_ni):
2816         if not self.changed_timestamp_match(target, prev_ni):
2817             try:
2818                 self.get_ninfo().csig = prev_ni.csig
2819             except AttributeError:
2820                 pass
2821             return False
2822         return self.changed_content(target, prev_ni)
2823
2824     def changed_timestamp_newer(self, target, prev_ni):
2825         try:
2826             return self.get_timestamp() > target.get_timestamp()
2827         except AttributeError:
2828             return 1
2829
2830     def changed_timestamp_match(self, target, prev_ni):
2831         try:
2832             return self.get_timestamp() != prev_ni.timestamp
2833         except AttributeError:
2834             return 1
2835
2836     def decide_source(self, target, prev_ni):
2837         return target.get_build_env().decide_source(self, target, prev_ni)
2838
2839     def decide_target(self, target, prev_ni):
2840         return target.get_build_env().decide_target(self, target, prev_ni)
2841
2842     # Initialize this Node's decider function to decide_source() because
2843     # every file is a source file until it has a Builder attached...
2844     changed_since_last_build = decide_source
2845
2846     def is_up_to_date(self):
2847         T = 0
2848         if T: Trace('is_up_to_date(%s):' % self)
2849         if not self.exists():
2850             if T: Trace(' not self.exists():')
2851             # The file doesn't exist locally...
2852             r = self.rfile()
2853             if r != self:
2854                 # ...but there is one in a Repository...
2855                 if not self.changed(r):
2856                     if T: Trace(' changed(%s):' % r)
2857                     # ...and it's even up-to-date...
2858                     if self._local:
2859                         # ...and they'd like a local copy.
2860                         e = LocalCopy(self, r, None)
2861                         if isinstance(e, SCons.Errors.BuildError):
2862                             raise 
2863                         self.store_info()
2864                     if T: Trace(' 1\n')
2865                     return 1
2866             self.changed()
2867             if T: Trace(' None\n')
2868             return None
2869         else:
2870             r = self.changed()
2871             if T: Trace(' self.exists():  %s\n' % r)
2872             return not r
2873
2874     memoizer_counters.append(SCons.Memoize.CountValue('rfile'))
2875
2876     def rfile(self):
2877         try:
2878             return self._memo['rfile']
2879         except KeyError:
2880             pass
2881         result = self
2882         if not self.exists():
2883             norm_name = _my_normcase(self.name)
2884             for dir in self.dir.get_all_rdirs():
2885                 try: node = dir.entries[norm_name]
2886                 except KeyError: node = dir.file_on_disk(self.name)
2887                 if node and node.exists() and \
2888                    (isinstance(node, File) or isinstance(node, Entry) \
2889                     or not node.is_derived()):
2890                         result = node
2891                         # Copy over our local attributes to the repository
2892                         # Node so we identify shared object files in the
2893                         # repository and don't assume they're static.
2894                         #
2895                         # This isn't perfect; the attribute would ideally
2896                         # be attached to the object in the repository in
2897                         # case it was built statically in the repository
2898                         # and we changed it to shared locally, but that's
2899                         # rarely the case and would only occur if you
2900                         # intentionally used the same suffix for both
2901                         # shared and static objects anyway.  So this
2902                         # should work well in practice.
2903                         result.attributes = self.attributes
2904                         break
2905         self._memo['rfile'] = result
2906         return result
2907
2908     def rstr(self):
2909         return str(self.rfile())
2910
2911     def get_cachedir_csig(self):
2912         """
2913         Fetch a Node's content signature for purposes of computing
2914         another Node's cachesig.
2915
2916         This is a wrapper around the normal get_csig() method that handles
2917         the somewhat obscure case of using CacheDir with the -n option.
2918         Any files that don't exist would normally be "built" by fetching
2919         them from the cache, but the normal get_csig() method will try
2920         to open up the local file, which doesn't exist because the -n
2921         option meant we didn't actually pull the file from cachedir.
2922         But since the file *does* actually exist in the cachedir, we
2923         can use its contents for the csig.
2924         """
2925         try:
2926             return self.cachedir_csig
2927         except AttributeError:
2928             pass
2929
2930         cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
2931         if not self.exists() and cachefile and os.path.exists(cachefile):
2932             self.cachedir_csig = SCons.Util.MD5filesignature(cachefile, \
2933                 SCons.Node.FS.File.md5_chunksize * 1024)
2934         else:
2935             self.cachedir_csig = self.get_csig()
2936         return self.cachedir_csig
2937
2938     def get_cachedir_bsig(self):
2939         try:
2940             return self.cachesig
2941         except AttributeError:
2942             pass
2943
2944         # Add the path to the cache signature, because multiple
2945         # targets built by the same action will all have the same
2946         # build signature, and we have to differentiate them somehow.
2947         children = self.children()
2948         executor = self.get_executor()
2949         # sigs = [n.get_cachedir_csig() for n in children]
2950         sigs = map(lambda n: n.get_cachedir_csig(), children)
2951         sigs.append(SCons.Util.MD5signature(executor.get_contents()))
2952         sigs.append(self.path)
2953         result = self.cachesig = SCons.Util.MD5collect(sigs)
2954         return result
2955
2956
2957 default_fs = None
2958
2959 def get_default_fs():
2960     global default_fs
2961     if not default_fs:
2962         default_fs = FS()
2963     return default_fs
2964
2965 class FileFinder:
2966     """
2967     """
2968     if SCons.Memoize.use_memoizer:
2969         __metaclass__ = SCons.Memoize.Memoized_Metaclass
2970
2971     memoizer_counters = []
2972
2973     def __init__(self):
2974         self._memo = {}
2975
2976     def filedir_lookup(self, p, fd=None):
2977         """
2978         A helper method for find_file() that looks up a directory for
2979         a file we're trying to find.  This only creates the Dir Node if
2980         it exists on-disk, since if the directory doesn't exist we know
2981         we won't find any files in it...  :-)
2982
2983         It would be more compact to just use this as a nested function
2984         with a default keyword argument (see the commented-out version
2985         below), but that doesn't work unless you have nested scopes,
2986         so we define it here just so this work under Python 1.5.2.
2987         """
2988         if fd is None:
2989             fd = self.default_filedir
2990         dir, name = os.path.split(fd)
2991         drive, d = os.path.splitdrive(dir)
2992         if d in ('/', os.sep):
2993             return p.fs.get_root(drive).dir_on_disk(name)
2994         if dir:
2995             p = self.filedir_lookup(p, dir)
2996             if not p:
2997                 return None
2998         norm_name = _my_normcase(name)
2999         try:
3000             node = p.entries[norm_name]
3001         except KeyError:
3002             return p.dir_on_disk(name)
3003         if isinstance(node, Dir):
3004             return node
3005         if isinstance(node, Entry):
3006             node.must_be_same(Dir)
3007             return node
3008         return None
3009
3010     def _find_file_key(self, filename, paths, verbose=None):
3011         return (filename, paths)
3012         
3013     memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key))
3014
3015     def find_file(self, filename, paths, verbose=None):
3016         """
3017         find_file(str, [Dir()]) -> [nodes]
3018
3019         filename - a filename to find
3020         paths - a list of directory path *nodes* to search in.  Can be
3021                 represented as a list, a tuple, or a callable that is
3022                 called with no arguments and returns the list or tuple.
3023
3024         returns - the node created from the found file.
3025
3026         Find a node corresponding to either a derived file or a file
3027         that exists already.
3028
3029         Only the first file found is returned, and none is returned
3030         if no file is found.
3031         """
3032         memo_key = self._find_file_key(filename, paths)
3033         try:
3034             memo_dict = self._memo['find_file']
3035         except KeyError:
3036             memo_dict = {}
3037             self._memo['find_file'] = memo_dict
3038         else:
3039             try:
3040                 return memo_dict[memo_key]
3041             except KeyError:
3042                 pass
3043
3044         if verbose and not callable(verbose):
3045             if not SCons.Util.is_String(verbose):
3046                 verbose = "find_file"
3047             verbose = '  %s: ' % verbose
3048             verbose = lambda s, v=verbose: sys.stdout.write(v + s)
3049
3050         filedir, filename = os.path.split(filename)
3051         if filedir:
3052             # More compact code that we can't use until we drop
3053             # support for Python 1.5.2:
3054             #
3055             #def filedir_lookup(p, fd=filedir):
3056             #    """
3057             #    A helper function that looks up a directory for a file
3058             #    we're trying to find.  This only creates the Dir Node
3059             #    if it exists on-disk, since if the directory doesn't
3060             #    exist we know we won't find any files in it...  :-)
3061             #    """
3062             #    dir, name = os.path.split(fd)
3063             #    if dir:
3064             #        p = filedir_lookup(p, dir)
3065             #        if not p:
3066             #            return None
3067             #    norm_name = _my_normcase(name)
3068             #    try:
3069             #        node = p.entries[norm_name]
3070             #    except KeyError:
3071             #        return p.dir_on_disk(name)
3072             #    if isinstance(node, Dir):
3073             #        return node
3074             #    if isinstance(node, Entry):
3075             #        node.must_be_same(Dir)
3076             #        return node
3077             #    if isinstance(node, Dir) or isinstance(node, Entry):
3078             #        return node
3079             #    return None
3080             #paths = filter(None, map(filedir_lookup, paths))
3081
3082             self.default_filedir = filedir
3083             paths = filter(None, map(self.filedir_lookup, paths))
3084
3085         result = None
3086         for dir in paths:
3087             if verbose:
3088                 verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
3089             node, d = dir.srcdir_find_file(filename)
3090             if node:
3091                 if verbose:
3092                     verbose("... FOUND '%s' in '%s'\n" % (filename, d))
3093                 result = node
3094                 break
3095
3096         memo_dict[memo_key] = result
3097
3098         return result
3099
3100 find_file = FileFinder().find_file
3101
3102
3103 def invalidate_node_memos(targets):
3104     """
3105     Invalidate the memoized values of all Nodes (files or directories)
3106     that are associated with the given entries. Has been added to
3107     clear the cache of nodes affected by a direct execution of an
3108     action (e.g.  Delete/Copy/Chmod). Existing Node caches become
3109     inconsistent if the action is run through Execute().  The argument
3110     `targets` can be a single Node object or filename, or a sequence
3111     of Nodes/filenames.
3112     """
3113     from traceback import extract_stack
3114
3115     # First check if the cache really needs to be flushed. Only
3116     # actions run in the SConscript with Execute() seem to be
3117     # affected. XXX The way to check if Execute() is in the stacktrace
3118     # is a very dirty hack and should be replaced by a more sensible
3119     # solution.
3120     for f in extract_stack():
3121         if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
3122             break
3123     else:
3124         # Dont have to invalidate, so return
3125         return
3126
3127     if not SCons.Util.is_List(targets):
3128         targets = [targets]
3129     
3130     for entry in targets:
3131         # If the target is a Node object, clear the cache. If it is a
3132         # filename, look up potentially existing Node object first.
3133         try:
3134             entry.clear_memoized_values()
3135         except AttributeError:
3136             # Not a Node object, try to look up Node by filename.  XXX
3137             # This creates Node objects even for those filenames which
3138             # do not correspond to an existing Node object.
3139             node = get_default_fs().Entry(entry)
3140             if node:
3141                 node.clear_memoized_values()                        
3142