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