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