http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / src / engine / SCons / Node / FS.py
index 964af62dddaffd5eb81c0176745bb9e8a39c760b..e5b81470d397e691199bd07c0d4c7620b0a122e6 100644 (file)
@@ -32,17 +32,72 @@ that can be used by scripts or modules looking for the canonical default.
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 #
+from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+from itertools import izip
+import cStringIO
+import fnmatch
 import os
 import os.path
+import re
 import shutil
 import stat
-import string
 import sys
 import time
-import cStringIO
+
+try:
+    import codecs
+except ImportError:
+    pass
+else:
+    # TODO(2.2):  Remove when 2.3 becomes the minimal supported version.
+    try:
+        codecs.BOM_UTF8
+    except AttributeError:
+        codecs.BOM_UTF8 = '\xef\xbb\xbf'
+    try:
+        codecs.BOM_UTF16_LE
+        codecs.BOM_UTF16_BE
+    except AttributeError:
+        codecs.BOM_UTF16_LE = '\xff\xfe'
+        codecs.BOM_UTF16_BE = '\xfe\xff'
+
+    # Provide a wrapper function to handle decoding differences in
+    # different versions of Python.  Normally, we'd try to do this in the
+    # compat layer (and maybe it still makes sense to move there?) but
+    # that doesn't provide a way to supply the string class used in
+    # pre-2.3 Python versions with a .decode() method that all strings
+    # naturally have.  Plus, the 2.[01] encodings behave differently
+    # enough that we have to settle for a lowest-common-denominator
+    # wrapper approach.
+    #
+    # Note that the 2.[012] implementations below may be inefficient
+    # because they perform an explicit look up of the encoding for every
+    # decode, but they're old enough (and we want to stop supporting
+    # them soon enough) that it's not worth complicating the interface.
+    # Think of it as additional incentive for people to upgrade...
+    try:
+        ''.decode
+    except AttributeError:
+        # 2.0 through 2.2:  strings have no .decode() method
+        try:
+            codecs.lookup('ascii').decode
+        except AttributeError:
+            # 2.0 and 2.1:  encodings are a tuple of functions, and the
+            # decode() function returns a (result, length) tuple.
+            def my_decode(contents, encoding):
+                return codecs.lookup(encoding)[1](contents)[0]
+        else:
+            # 2.2:  encodings are an object with methods, and the
+            # .decode() method returns just the decoded bytes.
+            def my_decode(contents, encoding):
+                return codecs.lookup(encoding).decode(contents)
+    else:
+        # 2.3 or later:  use the .decode() string method
+        def my_decode(contents, encoding):
+            return contents.decode(encoding)
 
 import SCons.Action
 from SCons.Debug import logInstanceCreation
@@ -56,6 +111,25 @@ import SCons.Warnings
 
 from SCons.Debug import Trace
 
+do_store_info = True
+
+
+class EntryProxyAttributeError(AttributeError):
+    """
+    An AttributeError subclass for recording and displaying the name
+    of the underlying Entry involved in an AttributeError exception.
+    """
+    def __init__(self, entry_proxy, attribute):
+        AttributeError.__init__(self)
+        self.entry_proxy = entry_proxy
+        self.attribute = attribute
+    def __str__(self):
+        entry = self.entry_proxy.get()
+        fmt = "%s instance %s has no attribute %s"
+        return fmt % (entry.__class__.__name__,
+                      repr(entry.name),
+                      repr(self.attribute))
+
 # The max_drift value:  by default, use a cached signature value for
 # any file that's been untouched for more than two days.
 default_max_drift = 2*24*60*60
@@ -71,10 +145,10 @@ default_max_drift = 2*24*60*60
 #
 # A number of the above factors, however, can be set after we've already
 # been asked to return a string for a Node, because a Repository() or
-# BuildDir() call or the like may not occur until later in SConscript
+# VariantDir() call or the like may not occur until later in SConscript
 # files.  So this variable controls whether we bother trying to save
 # string values for Nodes.  The wrapper interface can set this whenever
-# they're done mucking with Repository and BuildDir and the other stuff,
+# they're done mucking with Repository and VariantDir and the other stuff,
 # to let this module know it can start returning saved string values
 # for Nodes.
 #
@@ -84,6 +158,42 @@ def save_strings(val):
     global Save_Strings
     Save_Strings = val
 
+#
+# Avoid unnecessary function calls by recording a Boolean value that
+# tells us whether or not os.path.splitdrive() actually does anything
+# on this system, and therefore whether we need to bother calling it
+# when looking up path names in various methods below.
+# 
+
+do_splitdrive = None
+
+def initialize_do_splitdrive():
+    global do_splitdrive
+    drive, path = os.path.splitdrive('X:/foo')
+    do_splitdrive = not not drive
+
+initialize_do_splitdrive()
+
+#
+
+needs_normpath_check = None
+
+def initialize_normpath_check():
+    """
+    Initialize the normpath_check regular expression.
+
+    This function is used by the unit tests to re-initialize the pattern
+    when testing for behavior with different values of os.sep.
+    """
+    global needs_normpath_check
+    if os.sep == '/':
+        pattern = r'.*/|\.$|\.\.$'
+    else:
+        pattern = r'.*[/%s]|\.$|\.\.$' % re.escape(os.sep)
+    needs_normpath_check = re.compile(pattern)
+
+initialize_normpath_check()
+
 #
 # SCons.Action objects for interacting with the outside world.
 #
@@ -150,7 +260,7 @@ def set_duplicate(duplicate):
                                            "should be in Valid_Duplicates")
     global Link_Funcs
     Link_Funcs = []
-    for func in string.split(duplicate,'-'):
+    for func in duplicate.split('-'):
         if link_dict[func]:
             Link_Funcs.append(link_dict[func])
 
@@ -184,8 +294,6 @@ def LinkFunc(target, source, env):
             if func == Link_Funcs[-1]:
                 # exception of the last link method (copy) are fatal
                 raise
-            else:
-                pass
     return 0
 
 Link = SCons.Action.Action(LinkFunc, None)
@@ -265,7 +373,7 @@ if os.path.normcase("TeSt") == os.path.normpath("TeSt") and not _is_cygwin:
         return x
 else:
     def _my_normcase(x):
-        return string.upper(x)
+        return x.upper()
 
 
 
@@ -351,7 +459,7 @@ def set_diskcheck(list):
         dc.set(list)
 
 def diskcheck_types():
-    return map(lambda dc: dc.type, diskcheckers)
+    return [dc.type for dc in diskcheckers]
 
 
 
@@ -389,7 +497,7 @@ class EntryProxy(SCons.Util.Proxy):
             return self
         else:
             entry = self.get()
-            r = string.replace(entry.get_path(), os.sep, '/')
+            r = entry.get_path().replace(os.sep, '/')
             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
 
     def __get_windows_path(self):
@@ -399,7 +507,7 @@ class EntryProxy(SCons.Util.Proxy):
             return self
         else:
             entry = self.get()
-            r = string.replace(entry.get_path(), os.sep, '\\')
+            r = entry.get_path().replace(os.sep, '\\')
             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
 
     def __get_srcnode(self):
@@ -407,7 +515,7 @@ class EntryProxy(SCons.Util.Proxy):
 
     def __get_srcdir(self):
         """Returns the directory containing the source node linked to this
-        node via BuildDir(), or the directory of this node if not linked."""
+        node via VariantDir(), or the directory of this node if not linked."""
         return EntryProxy(self.get().srcnode().dir)
 
     def __get_rsrcnode(self):
@@ -415,7 +523,7 @@ class EntryProxy(SCons.Util.Proxy):
 
     def __get_rsrcdir(self):
         """Returns the directory containing the source node linked to this
-        node via BuildDir(), or the directory of this node if not linked."""
+        node via VariantDir(), or the directory of this node if not linked."""
         return EntryProxy(self.get().srcnode().rfile().dir)
 
     def __get_dir(self):
@@ -444,16 +552,11 @@ class EntryProxy(SCons.Util.Proxy):
         except KeyError:
             try:
                 attr = SCons.Util.Proxy.__getattr__(self, name)
-            except AttributeError:
-                entry = self.get()
-                classname = string.split(str(entry.__class__), '.')[-1]
-                if classname[-2:] == "'>":
-                    # new-style classes report their name as:
-                    #   "<class 'something'>"
-                    # instead of the classic classes:
-                    #   "something"
-                    classname = classname[:-2]
-                raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
+            except AttributeError, e:
+                # Raise our own AttributeError subclass with an
+                # overridden __str__() method that identifies the
+                # name of the entry that caused the exception.
+                raise EntryProxyAttributeError(self, name)
             return attr
         else:
             return attr_function(self)
@@ -483,34 +586,39 @@ class Base(SCons.Node.Node):
         if __debug__: logInstanceCreation(self, 'Node.FS.Base')
         SCons.Node.Node.__init__(self)
 
-        self.name = name
-        self.suffix = SCons.Util.splitext(name)[1]
+        # Filenames and paths are probably reused and are intern'ed to
+        # save some memory.
+        self.name = SCons.Util.silent_intern(name)
+        self.suffix = SCons.Util.silent_intern(SCons.Util.splitext(name)[1])
         self.fs = fs
 
         assert directory, "A directory must be provided"
 
-        self.abspath = directory.entry_abspath(name)
-        self.labspath = directory.entry_labspath(name)
+        self.abspath = SCons.Util.silent_intern(directory.entry_abspath(name))
+        self.labspath = SCons.Util.silent_intern(directory.entry_labspath(name))
         if directory.path == '.':
-            self.path = name
+            self.path = SCons.Util.silent_intern(name)
         else:
-            self.path = directory.entry_path(name)
+            self.path = SCons.Util.silent_intern(directory.entry_path(name))
         if directory.tpath == '.':
-            self.tpath = name
+            self.tpath = SCons.Util.silent_intern(name)
         else:
-            self.tpath = directory.entry_tpath(name)
+            self.tpath = SCons.Util.silent_intern(directory.entry_tpath(name))
         self.path_elements = directory.path_elements + [self]
 
         self.dir = directory
         self.cwd = None # will hold the SConscript directory for target nodes
         self.duplicate = directory.duplicate
 
+    def str_for_display(self):
+        return '"' + self.__str__() + '"'
+
     def must_be_same(self, klass):
         """
         This node, which already existed, is being looked up as the
         specified klass.  Raise an exception if it isn't.
         """
-        if self.__class__ is klass or klass is Entry:
+        if isinstance(self, klass) or klass is Entry:
             return
         raise TypeError, "Tried to lookup %s '%s' as a %s." %\
               (self.__class__.__name__, self.path, klass.__name__)
@@ -539,14 +647,34 @@ class Base(SCons.Node.Node):
             return self._memo['_save_str']
         except KeyError:
             pass
-        result = self._get_str()
+        result = intern(self._get_str())
         self._memo['_save_str'] = result
         return result
 
     def _get_str(self):
+        global Save_Strings
         if self.duplicate or self.is_derived():
             return self.get_path()
-        return self.srcnode().get_path()
+        srcnode = self.srcnode()
+        if srcnode.stat() is None and self.stat() is not None:
+            result = self.get_path()
+        else:
+            result = srcnode.get_path()
+        if not Save_Strings:
+            # We're not at the point where we're saving the string string
+            # representations of FS Nodes (because we haven't finished
+            # reading the SConscript files and need to have str() return
+            # things relative to them).  That also means we can't yet
+            # cache values returned (or not returned) by stat(), since
+            # Python code in the SConscript files might still create
+            # or otherwise affect the on-disk file.  So get rid of the
+            # values that the underlying stat() method saved.
+            try: del self._memo['stat']
+            except KeyError: pass
+            if self is not srcnode:
+                try: del srcnode._memo['stat']
+                except KeyError: pass
+        return result
 
     rstr = __str__
 
@@ -561,7 +689,7 @@ class Base(SCons.Node.Node):
         return result
 
     def exists(self):
-        return not self.stat() is None
+        return self.stat() is not None
 
     def rexists(self):
         return self.rfile().exists()
@@ -578,11 +706,11 @@ class Base(SCons.Node.Node):
 
     def isdir(self):
         st = self.stat()
-        return not st is None and stat.S_ISDIR(st[stat.ST_MODE])
+        return st is not None and stat.S_ISDIR(st[stat.ST_MODE])
 
     def isfile(self):
         st = self.stat()
-        return not st is None and stat.S_ISREG(st[stat.ST_MODE])
+        return st is not None and stat.S_ISREG(st[stat.ST_MODE])
 
     if hasattr(os, 'symlink'):
         def islink(self):
@@ -607,15 +735,11 @@ class Base(SCons.Node.Node):
         corresponding to its source file.  Otherwise, return
         ourself.
         """
-        dir=self.dir
-        name=self.name
-        while dir:
-            if dir.srcdir:
-                srcnode = dir.srcdir.Entry(name)
-                srcnode.must_be_same(self.__class__)
-                return srcnode
-            name = dir.name + os.sep + name
-            dir = dir.up()
+        srcdir_list = self.dir.srcdir_list()
+        if srcdir_list:
+            srcnode = srcdir_list[0].Entry(self.name)
+            srcnode.must_be_same(self.__class__)
+            return srcnode
         return self
 
     def get_path(self, dir=None):
@@ -629,8 +753,8 @@ class Base(SCons.Node.Node):
         try: i = path_elems.index(dir)
         except ValueError: pass
         else: path_elems = path_elems[i+1:]
-        path_elems = map(lambda n: n.name, path_elems)
-        return string.join(path_elems, os.sep)
+        path_elems = [n.name for n in path_elems]
+        return os.sep.join(path_elems)
 
     def set_src_builder(self, builder):
         """Set the source code builder for this node."""
@@ -673,7 +797,7 @@ class Base(SCons.Node.Node):
     def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext):
         """
 
-       Generates a target entry that corresponds to this entry (usually
+        Generates a target entry that corresponds to this entry (usually
         a source file) with the specified prefix and suffix.
 
         Note that this method can be overridden dynamically for generated
@@ -745,6 +869,9 @@ class Base(SCons.Node.Node):
         self._memo['rentry'] = result
         return result
 
+    def _glob1(self, pattern, ondisk=True, source=False, strings=False):
+        return []
+
 class Entry(Base):
     """This is the class for generic Node.FS entries--that is, things
     that could be a File or a Dir, but we're just not sure yet.
@@ -803,11 +930,8 @@ class Entry(Base):
         return self.get_suffix()
 
     def get_contents(self):
-        """Fetch the contents of the entry.
-
-        Since this should return the real contents from the file
-        system, we check to see into what sort of subclass we should
-        morph this Entry."""
+        """Fetch the contents of the entry.  Returns the exact binary
+        contents of the file."""
         try:
             self = self.disambiguate(must_exist=1)
         except SCons.Errors.UserError:
@@ -820,13 +944,31 @@ class Entry(Base):
         else:
             return self.get_contents()
 
+    def get_text_contents(self):
+        """Fetch the decoded text contents of a Unicode encoded Entry.
+
+        Since this should return the text contents from the file
+        system, we check to see into what sort of subclass we should
+        morph this Entry."""
+        try:
+            self = self.disambiguate(must_exist=1)
+        except SCons.Errors.UserError:
+            # There was nothing on disk with which to disambiguate
+            # this entry.  Leave it as an Entry, but return a null
+            # string so calls to get_text_contents() in emitters and
+            # the like (e.g. in qt.py) don't have to disambiguate by
+            # hand or catch the exception.
+            return ''
+        else:
+            return self.get_text_contents()
+
     def must_be_same(self, klass):
         """Called to make sure a Node is a Dir.  Since we're an
         Entry, we can morph into one."""
-        if not self.__class__ is klass:
+        if self.__class__ is not klass:
             self.__class__ = klass
             self._morph()
-            self.clear
+            self.clear()
 
     # The following methods can get called before the Taskmaster has
     # had a chance to call disambiguate() directly to see if this Entry
@@ -845,11 +987,11 @@ class Entry(Base):
         directory."""
         return self.disambiguate().exists()
 
-#    def rel_path(self, other):
-#        d = self.disambiguate()
-#        if d.__class__ == Entry:
-#            raise "rel_path() could not disambiguate File/Dir"
-#        return d.rel_path(other)
+    def rel_path(self, other):
+        d = self.disambiguate()
+        if d.__class__ is Entry:
+            raise "rel_path() could not disambiguate File/Dir"
+        return d.rel_path(other)
 
     def new_ninfo(self):
         return self.disambiguate().new_ninfo()
@@ -857,6 +999,12 @@ class Entry(Base):
     def changed_since_last_build(self, target, prev_ni):
         return self.disambiguate().changed_since_last_build(target, prev_ni)
 
+    def _glob1(self, pattern, ondisk=True, source=False, strings=False):
+        return self.disambiguate()._glob1(pattern, ondisk, source, strings)
+
+    def get_subst_proxy(self):
+        return self.disambiguate().get_subst_proxy()
+
 # This is for later so we can differentiate between Entry the class and Entry
 # the method of the FS class.
 _classEntry = Entry
@@ -885,6 +1033,8 @@ class LocalFS:
     #    return os.chdir(path)
     def chmod(self, path, mode):
         return os.chmod(path, mode)
+    def copy(self, src, dst):
+        return shutil.copy(src, dst)
     def copy2(self, src, dst):
         return shutil.copy2(src, dst)
     def exists(self, path):
@@ -975,8 +1125,8 @@ class FS(LocalFS):
         self.Top.tpath = '.'
         self._cwd = self.Top
 
-        DirNodeInfo.top = self.Top
-        FileNodeInfo.top = self.Top
+        DirNodeInfo.fs = self
+        FileNodeInfo.fs = self
     
     def set_SConstruct_dir(self, dir):
         self.SConstruct_dir = dir
@@ -997,7 +1147,7 @@ class FS(LocalFS):
         """
         curr=self._cwd
         try:
-            if not dir is None:
+            if dir is not None:
                 self._cwd = dir
                 if change_os_dir:
                     os.chdir(dir.abspath)
@@ -1028,11 +1178,16 @@ class FS(LocalFS):
 
         This translates arbitrary input into a canonical Node.FS object
         of the specified fsclass.  The general approach for strings is
-        to turn it into a normalized absolute path and then call the
-        root directory's lookup_abs() method for the heavy lifting.
+        to turn it into a fully normalized absolute path and then call
+        the root directory's lookup_abs() method for the heavy lifting.
 
         If the path name begins with '#', it is unconditionally
-        interpreted relative to the top-level directory of this FS.
+        interpreted relative to the top-level directory of this FS.  '#'
+        is treated as a synonym for the top-level SConstruct directory,
+        much like '~' is treated as a synonym for the user's home
+        directory in a UNIX shell.  So both '#foo' and '#/foo' refer
+        to the 'foo' subdirectory underneath the top-level SConstruct
+        directory.
 
         If the path name is relative, then the path is looked up relative
         to the specified directory, or the current directory (self._cwd,
@@ -1046,39 +1201,59 @@ class FS(LocalFS):
             return p
         # str(p) in case it's something like a proxy object
         p = str(p)
-        drive, p = os.path.splitdrive(p)
+
+        initial_hash = (p[0:1] == '#')
+        if initial_hash:
+            # There was an initial '#', so we strip it and override
+            # whatever directory they may have specified with the
+            # top-level SConstruct directory.
+            p = p[1:]
+            directory = self.Top
+
+        if directory and not isinstance(directory, Dir):
+            directory = self.Dir(directory)
+
+        if do_splitdrive:
+            drive, p = os.path.splitdrive(p)
+        else:
+            drive = ''
         if drive and not p:
-            # A drive letter without a path...
+            # This causes a naked drive letter to be treated as a synonym
+            # for the root directory on that drive.
             p = os.sep
-            root = self.get_root(drive)
-        elif os.path.isabs(p):
-            # An absolute path...
+        absolute = os.path.isabs(p)
+
+        needs_normpath = needs_normpath_check.match(p)
+
+        if initial_hash or not absolute:
+            # This is a relative lookup, either to the top-level
+            # SConstruct directory (because of the initial '#') or to
+            # the current directory (the path name is not absolute).
+            # Add the string to the appropriate directory lookup path,
+            # after which the whole thing gets normalized.
+            if not directory:
+                directory = self._cwd
+            if p:
+                p = directory.labspath + '/' + p
+            else:
+                p = directory.labspath
+
+        if needs_normpath:
             p = os.path.normpath(p)
+
+        if drive or absolute:
             root = self.get_root(drive)
         else:
-            if p[0:1] == '#':
-                # A top-relative path...
-                directory = self.Top
-                offset = 1
-                if p[1:2] in(os.sep, '/'):
-                    offset = 2
-                p = p[offset:]
-            else:
-                # A relative path...
-                if not directory:
-                    # ...to the current (SConscript) directory.
-                    directory = self._cwd
-                elif not isinstance(directory, Dir):
-                    # ...to the specified directory.
-                    directory = self.Dir(directory)
-            p = os.path.normpath(directory.labspath + '/' + p)
+            if not directory:
+                directory = self._cwd
             root = directory.root
+
         if os.sep != '/':
-            p = string.replace(p, os.sep, '/')
+            p = p.replace(os.sep, '/')
         return root._lookup_abs(p, fsclass, create)
 
     def Entry(self, name, directory = None, create = 1):
-        """Lookup or create a generic Entry node with the specified name.
+        """Look up or create a generic Entry node with the specified name.
         If the name is a relative path (begins with ./, ../, or a file
         name), then it is looked up relative to the supplied directory
         node, or to the top level directory of the FS (supplied at
@@ -1087,7 +1262,7 @@ class FS(LocalFS):
         return self._lookup(name, directory, Entry, create)
 
     def File(self, name, directory = None, create = 1):
-        """Lookup or create a File node with the specified name.  If
+        """Look up or create a File node with the specified name.  If
         the name is a relative path (begins with ./, ../, or a file name),
         then it is looked up relative to the supplied directory node,
         or to the top level directory of the FS (supplied at construction
@@ -1098,8 +1273,8 @@ class FS(LocalFS):
         """
         return self._lookup(name, directory, File, create)
 
-    def Dir(self, name, directory = None, create = 1):
-        """Lookup or create a Dir node with the specified name.  If
+    def Dir(self, name, directory = None, create = True):
+        """Look up or create a Dir node with the specified name.  If
         the name is a relative path (begins with ./, ../, or a file name),
         then it is looked up relative to the supplied directory node,
         or to the top level directory of the FS (supplied at construction
@@ -1110,21 +1285,21 @@ class FS(LocalFS):
         """
         return self._lookup(name, directory, Dir, create)
 
-    def BuildDir(self, build_dir, src_dir, duplicate=1):
-        """Link the supplied build directory to the source directory
+    def VariantDir(self, variant_dir, src_dir, duplicate=1):
+        """Link the supplied variant directory to the source directory
         for purposes of building files."""
 
         if not isinstance(src_dir, SCons.Node.Node):
             src_dir = self.Dir(src_dir)
-        if not isinstance(build_dir, SCons.Node.Node):
-            build_dir = self.Dir(build_dir)
-        if src_dir.is_under(build_dir):
-            raise SCons.Errors.UserError, "Source directory cannot be under build directory."
-        if build_dir.srcdir:
-            if build_dir.srcdir == src_dir:
+        if not isinstance(variant_dir, SCons.Node.Node):
+            variant_dir = self.Dir(variant_dir)
+        if src_dir.is_under(variant_dir):
+            raise SCons.Errors.UserError, "Source directory cannot be under variant directory."
+        if variant_dir.srcdir:
+            if variant_dir.srcdir == src_dir:
                 return # We already did this.
-            raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(build_dir, build_dir.srcdir)
-        build_dir.link(src_dir, duplicate)
+            raise SCons.Errors.UserError, "'%s' already has a source directory: '%s'."%(variant_dir, variant_dir.srcdir)
+        variant_dir.link(src_dir, duplicate)
 
     def Repository(self, *dirs):
         """Specify Repository directories to search."""
@@ -1133,11 +1308,11 @@ class FS(LocalFS):
                 d = self.Dir(d)
             self.Top.addRepository(d)
 
-    def build_dir_target_climb(self, orig, dir, tail):
-        """Create targets in corresponding build directories
+    def variant_dir_target_climb(self, orig, dir, tail):
+        """Create targets in corresponding variant directories
 
         Climb the directory tree, and look up path names
-        relative to any linked build directories we find.
+        relative to any linked variant directories we find.
 
         Even though this loops and walks up the tree, we don't memoize
         the return value because this is really only used to process
@@ -1145,39 +1320,56 @@ class FS(LocalFS):
         """
         targets = []
         message = None
-        fmt = "building associated BuildDir targets: %s"
+        fmt = "building associated VariantDir targets: %s"
         start_dir = dir
         while dir:
-            for bd in dir.build_dirs:
+            for bd in dir.variant_dirs:
                 if start_dir.is_under(bd):
                     # If already in the build-dir location, don't reflect
                     return [orig], fmt % str(orig)
-                p = apply(os.path.join, [bd.path] + tail)
+                p = os.path.join(bd.path, *tail)
                 targets.append(self.Entry(p))
             tail = [dir.name] + tail
             dir = dir.up()
         if targets:
-            message = fmt % string.join(map(str, targets))
+            message = fmt % ' '.join(map(str, targets))
         return targets, message
 
+    def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None):
+        """
+        Globs
+
+        This is mainly a shim layer 
+        """
+        if cwd is None:
+            cwd = self.getcwd()
+        return cwd.glob(pathname, ondisk, source, strings)
+
 class DirNodeInfo(SCons.Node.NodeInfoBase):
     # This should get reset by the FS initialization.
     current_version_id = 1
 
-    top = None
+    fs = None
 
     def str_to_node(self, s):
-        top = self.top
-        if os.path.isabs(s):
-            n = top.fs._lookup(s, top, Entry)
-        else:
+        top = self.fs.Top
+        root = top.root
+        if do_splitdrive:
+            drive, s = os.path.splitdrive(s)
+            if drive:
+                root = self.fs.get_root(drive)
+        if not os.path.isabs(s):
             s = top.labspath + '/' + s
-            n = top.root._lookup_abs(s, Entry)
-        return n
+        return root._lookup_abs(s, Entry)
 
 class DirBuildInfo(SCons.Node.BuildInfoBase):
     current_version_id = 1
 
+glob_magic_check = re.compile('[*?[]')
+
+def has_glob_magic(s):
+    return glob_magic_check.search(s) is not None
+
 class Dir(Base):
     """A class for directories in a file system.
     """
@@ -1210,7 +1402,7 @@ class Dir(Base):
         self.cwd = self
         self.searched = 0
         self._sconsign = None
-        self.build_dirs = []
+        self.variant_dirs = []
         self.root = self.dir.root
 
         # Don't just reset the executor, replace its action list,
@@ -1238,7 +1430,7 @@ class Dir(Base):
                         del node._srcreps
                     except AttributeError:
                         pass
-                    if duplicate != None:
+                    if duplicate is not None:
                         node.duplicate=duplicate
 
     def __resetDuplicate(self, node):
@@ -1252,13 +1444,12 @@ class Dir(Base):
         """
         return self.fs.Entry(name, self)
 
-    def Dir(self, name):
+    def Dir(self, name, create=True):
         """
         Looks up or creates a directory node named 'name' relative to
         this directory.
         """
-        dir = self.fs.Dir(name, self)
-        return dir
+        return self.fs.Dir(name, self, create)
 
     def File(self, name):
         """
@@ -1281,16 +1472,16 @@ class Dir(Base):
         a path containing '..'), an absolute path name, a top-relative
         ('#foo') path name, or any kind of object.
         """
-        name = self.labspath + '/' + name
+        name = self.entry_labspath(name)
         return self.root._lookup_abs(name, klass, create)
 
     def link(self, srcdir, duplicate):
-        """Set this directory as the build directory for the
+        """Set this directory as the variant directory for the
         supplied source directory."""
         self.srcdir = srcdir
         self.duplicate = duplicate
         self.__clearRepositoryCache(duplicate)
-        srcdir.build_dirs.append(self)
+        srcdir.variant_dirs.append(self)
 
     def getRepositories(self):
         """Returns a list of repositories for this directory.
@@ -1303,7 +1494,7 @@ class Dir(Base):
 
     def get_all_rdirs(self):
         try:
-            return self._memo['get_all_rdirs']
+            return list(self._memo['get_all_rdirs'])
         except KeyError:
             pass
 
@@ -1313,10 +1504,13 @@ class Dir(Base):
         while dir:
             for rep in dir.getRepositories():
                 result.append(rep.Dir(fname))
-            fname = dir.name + os.sep + fname
+            if fname == '.':
+                fname = dir.name
+            else:
+                fname = dir.name + os.sep + fname
             dir = dir.up()
 
-        self._memo['get_all_rdirs'] = result
+        self._memo['get_all_rdirs'] = list(result)
 
         return result
 
@@ -1329,66 +1523,64 @@ class Dir(Base):
     def up(self):
         return self.entries['..']
 
-# This complicated method, which constructs relative paths between
-# arbitrary Node.FS objects, is no longer used.  It was introduced to
-# store dependency paths in .sconsign files relative to the target, but
-# that ended up being significantly inefficient.  We're leaving the code
-# here, commented out, because it would be too easy for someone to decide
-# to re-invent this wheel in the future (if it becomes necessary) because
-# they didn't know this code was buried in some source-code change from
-# the distant past...
-#
-#    def _rel_path_key(self, other):
-#        return str(other)
-#
-#    memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
-#
-#    def rel_path(self, other):
-#        """Return a path to "other" relative to this directory.
-#        """
-#        try:
-#            memo_dict = self._memo['rel_path']
-#        except KeyError:
-#            memo_dict = {}
-#            self._memo['rel_path'] = memo_dict
-#        else:
-#            try:
-#                return memo_dict[other]
-#            except KeyError:
-#                pass
-#
-#        if self is other:
-#
-#            result = '.'
-#
-#        elif not other in self.path_elements:
-#
-#            try:
-#                other_dir = other.get_dir()
-#            except AttributeError:
-#                result = str(other)
-#            else:
-#                if other_dir is None:
-#                    result = other.name
-#                else:
-#                    dir_rel_path = self.rel_path(other_dir)
-#                    if dir_rel_path == '.':
-#                        result = other.name
-#                    else:
-#                        result = dir_rel_path + os.sep + other.name
-#
-#        else:
-#
-#            i = self.path_elements.index(other) + 1
-#
-#            path_elems = ['..'] * (len(self.path_elements) - i) \
-#                         + map(lambda n: n.name, other.path_elements[i:])
-#             
-#            result = string.join(path_elems, os.sep)
-#
-#        memo_dict[other] = result
-#
-#        return result
+    def _rel_path_key(self, other):
+        return str(other)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key))
+
+    def rel_path(self, other):
+        """Return a path to "other" relative to this directory.
+        """
+
+        # This complicated and expensive method, which constructs relative
+        # paths between arbitrary Node.FS objects, is no longer used
+        # by SCons itself.  It was introduced to store dependency paths
+        # in .sconsign files relative to the target, but that ended up
+        # being significantly inefficient.
+        #
+        # We're continuing to support the method because some SConstruct
+        # files out there started using it when it was available, and
+        # we're all about backwards compatibility..
+
+        try:
+            memo_dict = self._memo['rel_path']
+        except KeyError:
+            memo_dict = {}
+            self._memo['rel_path'] = memo_dict
+        else:
+            try:
+                return memo_dict[other]
+            except KeyError:
+                pass
+
+        if self is other:
+            result = '.'
+
+        elif not other in self.path_elements:
+            try:
+                other_dir = other.get_dir()
+            except AttributeError:
+                result = str(other)
+            else:
+                if other_dir is None:
+                    result = other.name
+                else:
+                    dir_rel_path = self.rel_path(other_dir)
+                    if dir_rel_path == '.':
+                        result = other.name
+                    else:
+                        result = dir_rel_path + os.sep + other.name
+        else:
+            i = self.path_elements.index(other) + 1
+
+            path_elems = ['..'] * (len(self.path_elements) - i) \
+                         + [n.name for n in other.path_elements[i:]]
+             
+            result = os.sep.join(path_elems)
+
+        memo_dict[other] = result
+
+        return result
 
     def get_env_scanner(self, env, kw={}):
         import SCons.Defaults
@@ -1429,8 +1621,8 @@ class Dir(Base):
     def build(self, **kw):
         """A null "builder" for directories."""
         global MkdirBuilder
-        if not self.builder is MkdirBuilder:
-            apply(SCons.Node.Node.build, [self,], kw)
+        if self.builder is not MkdirBuilder:
+            SCons.Node.Node.build(self, **kw)
 
     #
     #
@@ -1447,6 +1639,8 @@ class Dir(Base):
             listDirs.append(parent)
             p = parent.up()
             if p is None:
+                # Don't use while: - else: for this condition because
+                # if so, then parent is None and has no .path attribute.
                 raise SCons.Errors.StopError, parent.path
             parent = p
         listDirs.reverse()
@@ -1468,21 +1662,38 @@ class Dir(Base):
 
     def multiple_side_effect_has_builder(self):
         global MkdirBuilder
-        return not self.builder is MkdirBuilder and self.has_builder()
+        return self.builder is not MkdirBuilder and self.has_builder()
 
     def alter_targets(self):
-        """Return any corresponding targets in a build directory.
+        """Return any corresponding targets in a variant directory.
         """
-        return self.fs.build_dir_target_climb(self, self, [])
+        return self.fs.variant_dir_target_climb(self, self, [])
 
     def scanner_key(self):
         """A directory does not get scanned."""
         return None
 
+    def get_text_contents(self):
+        """We already emit things in text, so just return the binary
+        version."""
+        return self.get_contents()
+
     def get_contents(self):
-        """Return aggregate contents of all our children."""
-        contents = map(lambda n: n.get_contents(), self.children())
-        return  string.join(contents, '')
+        """Return content signatures and names of all our children
+        separated by new-lines. Ensure that the nodes are sorted."""
+        contents = []
+        for node in sorted(self.children(), key=lambda t: t.name):
+            contents.append('%s %s\n' % (node.get_csig(), node.name))
+        return ''.join(contents)
+
+    def get_csig(self):
+        """Compute the content signature for Directory nodes. In
+        general, this is not needed and the content signature is not
+        stored in the DirNodeInfo. However, if get_contents on a Dir
+        node is called which has a child directory, the child
+        directory should return the hash of its contents."""
+        contents = self.get_contents()
+        return SCons.Util.MD5signature(contents)
 
     def do_duplicate(self, src):
         pass
@@ -1492,7 +1703,7 @@ class Dir(Base):
     def is_up_to_date(self):
         """If any child is not up-to-date, then this directory isn't,
         either."""
-        if not self.builder is MkdirBuilder and not self.exists():
+        if self.builder is not MkdirBuilder and not self.exists():
             return 0
         up_to_date = SCons.Node.up_to_date
         for kid in self.children():
@@ -1557,9 +1768,19 @@ class Dir(Base):
                 pass
             else:
                 for entry in map(_my_normcase, entries):
-                    d[entry] = 1
+                    d[entry] = True
             self.on_disk_entries = d
-        return d.has_key(_my_normcase(name))
+        if sys.platform == 'win32':
+            name = _my_normcase(name)
+            result = d.get(name)
+            if result is None:
+                # Belt-and-suspenders for Windows:  check directly for
+                # 8.3 file names that don't show up in os.listdir().
+                result = os.path.exists(self.abspath + os.sep + name)
+                d[name] = result
+            return result
+        else:
+            return name in d
 
     memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list'))
 
@@ -1575,13 +1796,7 @@ class Dir(Base):
         dir = self
         while dir:
             if dir.srcdir:
-                d = dir.srcdir.Dir(dirname)
-                if d.is_under(dir):
-                    # Shouldn't source from something in the build path:
-                    # build_dir is probably under src_dir, in which case
-                    # we are reflecting.
-                    break
-                result.append(d)
+                result.append(dir.srcdir.Dir(dirname))
             dirname = dir.name + os.sep + dirname
             dir = dir.up()
 
@@ -1591,6 +1806,11 @@ class Dir(Base):
 
     def srcdir_duplicate(self, name):
         for dir in self.srcdir_list():
+            if self.is_under(dir):
+                # We shouldn't source from something in the build path;
+                # variant_dir is probably under src_dir, in which case
+                # we are reflecting.
+                break
             if dir.entry_exists_on_disk(name):
                 srcnode = dir.Entry(name).disambiguate()
                 if self.duplicate:
@@ -1653,7 +1873,10 @@ class Dir(Base):
         if self.entry_exists_on_disk(name):
             try: return self.Dir(name)
             except TypeError: pass
-        return None
+        node = self.srcdir_duplicate(name)
+        if isinstance(node, File):
+            return None
+        return node
 
     def file_on_disk(self, name):
         if self.entry_exists_on_disk(name) or \
@@ -1663,7 +1886,7 @@ class Dir(Base):
             except TypeError: pass
         node = self.srcdir_duplicate(name)
         if isinstance(node, Dir):
-            node = None
+            return None
         return node
 
     def walk(self, func, arg):
@@ -1689,10 +1912,128 @@ class Dir(Base):
         names.remove('.')
         names.remove('..')
         func(arg, self, names)
-        select_dirs = lambda n, e=entries: isinstance(e[n], Dir)
-        for dirname in filter(select_dirs, names):
+        for dirname in [n for n in names if isinstance(entries[n], Dir)]:
             entries[dirname].walk(func, arg)
 
+    def glob(self, pathname, ondisk=True, source=False, strings=False):
+        """
+        Returns a list of Nodes (or strings) matching a specified
+        pathname pattern.
+
+        Pathname patterns follow UNIX shell semantics:  * matches
+        any-length strings of any characters, ? matches any character,
+        and [] can enclose lists or ranges of characters.  Matches do
+        not span directory separators.
+
+        The matches take into account Repositories, returning local
+        Nodes if a corresponding entry exists in a Repository (either
+        an in-memory Node or something on disk).
+
+        By defafult, the glob() function matches entries that exist
+        on-disk, in addition to in-memory Nodes.  Setting the "ondisk"
+        argument to False (or some other non-true value) causes the glob()
+        function to only match in-memory Nodes.  The default behavior is
+        to return both the on-disk and in-memory Nodes.
+
+        The "source" argument, when true, specifies that corresponding
+        source Nodes must be returned if you're globbing in a build
+        directory (initialized with VariantDir()).  The default behavior
+        is to return Nodes local to the VariantDir().
+
+        The "strings" argument, when true, returns the matches as strings,
+        not Nodes.  The strings are path names relative to this directory.
+
+        The underlying algorithm is adapted from the glob.glob() function
+        in the Python library (but heavily modified), and uses fnmatch()
+        under the covers.
+        """
+        dirname, basename = os.path.split(pathname)
+        if not dirname:
+            return sorted(self._glob1(basename, ondisk, source, strings),
+                          key=lambda t: str(t))
+        if has_glob_magic(dirname):
+            list = self.glob(dirname, ondisk, source, strings=False)
+        else:
+            list = [self.Dir(dirname, create=True)]
+        result = []
+        for dir in list:
+            r = dir._glob1(basename, ondisk, source, strings)
+            if strings:
+                r = [os.path.join(str(dir), x) for x in r]
+            result.extend(r)
+        result.sort(lambda a, b: cmp(str(a), str(b)))
+        return result
+
+    def _glob1(self, pattern, ondisk=True, source=False, strings=False):
+        """
+        Globs for and returns a list of entry names matching a single
+        pattern in this directory.
+
+        This searches any repositories and source directories for
+        corresponding entries and returns a Node (or string) relative
+        to the current directory if an entry is found anywhere.
+
+        TODO: handle pattern with no wildcard
+        """
+        search_dir_list = self.get_all_rdirs()
+        for srcdir in self.srcdir_list():
+            search_dir_list.extend(srcdir.get_all_rdirs())
+
+        selfEntry = self.Entry
+        names = []
+        for dir in search_dir_list:
+            # We use the .name attribute from the Node because the keys of
+            # the dir.entries dictionary are normalized (that is, all upper
+            # case) on case-insensitive systems like Windows.
+            node_names = [ v.name for k, v in dir.entries.items()
+                           if k not in ('.', '..') ]
+            names.extend(node_names)
+            if not strings:
+                # Make sure the working directory (self) actually has
+                # entries for all Nodes in repositories or variant dirs.
+                for name in node_names: selfEntry(name)
+            if ondisk:
+                try:
+                    disk_names = os.listdir(dir.abspath)
+                except os.error:
+                    continue
+                names.extend(disk_names)
+                if not strings:
+                    # We're going to return corresponding Nodes in
+                    # the local directory, so we need to make sure
+                    # those Nodes exist.  We only want to create
+                    # Nodes for the entries that will match the
+                    # specified pattern, though, which means we
+                    # need to filter the list here, even though
+                    # the overall list will also be filtered later,
+                    # after we exit this loop.
+                    if pattern[0] != '.':
+                        #disk_names = [ d for d in disk_names if d[0] != '.' ]
+                        disk_names = [x for x in disk_names if x[0] != '.']
+                    disk_names = fnmatch.filter(disk_names, pattern)
+                    dirEntry = dir.Entry
+                    for name in disk_names:
+                        # Add './' before disk filename so that '#' at
+                        # beginning of filename isn't interpreted.
+                        name = './' + name
+                        node = dirEntry(name).disambiguate()
+                        n = selfEntry(name)
+                        if n.__class__ != node.__class__:
+                            n.__class__ = node.__class__
+                            n._morph()
+
+        names = set(names)
+        if pattern[0] != '.':
+            #names = [ n for n in names if n[0] != '.' ]
+            names = [x for x in names if x[0] != '.']
+        names = fnmatch.filter(names, pattern)
+
+        if strings:
+            return names
+
+        #return [ self.entries[_my_normcase(n)] for n in names ]
+        return [self.entries[_my_normcase(n)] for n in names]
+
 class RootDir(Dir):
     """A class for the root directory of a file system.
 
@@ -1720,7 +2061,7 @@ class RootDir(Dir):
         # except for the "lookup abspath," which does not have the
         # drive letter.
         self.abspath = name + os.sep
-        self.labspath = '/'
+        self.labspath = ''
         self.path = name + os.sep
         self.tpath = name + os.sep
         self._morph()
@@ -1731,6 +2072,7 @@ class RootDir(Dir):
         # os.path.normpath() seems to preserve double slashes at the
         # beginning of a path (presumably for UNC path names), but
         # collapses triple slashes to a single slash.
+        self._lookupDict[''] = self
         self._lookupDict['/'] = self
         self._lookupDict['//'] = self
         self._lookupDict[os.sep] = self
@@ -1762,19 +2104,21 @@ class RootDir(Dir):
             result = self._lookupDict[k]
         except KeyError:
             if not create:
-                raise SCons.Errors.UserError
+                msg = "No such file or directory: '%s' in '%s' (and create is False)" % (p, str(self))
+                raise SCons.Errors.UserError, msg
             # There is no Node for this path name, and we're allowed
             # to create it.
             dir_name, file_name = os.path.split(p)
             dir_node = self._lookup_abs(dir_name, Dir)
             result = klass(file_name, dir_node, self.fs)
-            self._lookupDict[k] = result
-            dir_node.entries[_my_normcase(file_name)] = result
-            dir_node.implicit = None
 
             # Double-check on disk (as configured) that the Node we
             # created matches whatever is out there in the real world.
             result.diskcheck_match()
+
+            self._lookupDict[k] = result
+            dir_node.entries[_my_normcase(file_name)] = result
+            dir_node.implicit = None
         else:
             # There is already a Node for this path name.  Allow it to
             # complain if we were looking for an inappropriate type.
@@ -1788,7 +2132,7 @@ class RootDir(Dir):
         return self.abspath + name
 
     def entry_labspath(self, name):
-        return self.labspath + name
+        return '/' + name
 
     def entry_path(self, name):
         return self.path + name
@@ -1817,16 +2161,18 @@ class FileNodeInfo(SCons.Node.NodeInfoBase):
     field_list = ['csig', 'timestamp', 'size']
 
     # This should get reset by the FS initialization.
-    top = None
+    fs = None
 
     def str_to_node(self, s):
-        top = self.top
-        if os.path.isabs(s):
-            n = top.fs._lookup(s, top, Entry)
-        else:
+        top = self.fs.Top
+        root = top.root
+        if do_splitdrive:
+            drive, s = os.path.splitdrive(s)
+            if drive:
+                root = self.fs.get_root(drive)
+        if not os.path.isabs(s):
             s = top.labspath + '/' + s
-            n = top.root._lookup_abs(s, Entry)
-        return n
+        return root._lookup_abs(s, Entry)
 
 class FileBuildInfo(SCons.Node.BuildInfoBase):
     current_version_id = 1
@@ -1848,7 +2194,7 @@ class FileBuildInfo(SCons.Node.BuildInfoBase):
                 except AttributeError:
                     s = str(n)
                 else:
-                    s = string.replace(s, os.sep, '/')
+                    s = s.replace(os.sep, '/')
                 return s
         for attr in ['bsources', 'bdepends', 'bimplicit']:
             try:
@@ -1856,7 +2202,7 @@ class FileBuildInfo(SCons.Node.BuildInfoBase):
             except AttributeError:
                 pass
             else:
-                setattr(self, attr, map(node_to_str, val))
+                setattr(self, attr, list(map(node_to_str, val)))
     def convert_from_sconsign(self, dir, name):
         """
         Converts a newly-read FileBuildInfo object for in-SCons use
@@ -1884,23 +2230,22 @@ class FileBuildInfo(SCons.Node.BuildInfoBase):
                 strings = getattr(self, nattr)
                 nodeinfos = getattr(self, sattr)
             except AttributeError:
-                pass
-            else:
-                nodes = []
-                for s, ni in zip(strings, nodeinfos):
-                    if not isinstance(s, SCons.Node.Node):
-                        s = ni.str_to_node(s)
-                    nodes.append(s)
-                setattr(self, nattr, nodes)
+                continue
+            nodes = []
+            for s, ni in izip(strings, nodeinfos):
+                if not isinstance(s, SCons.Node.Node):
+                    s = ni.str_to_node(s)
+                nodes.append(s)
+            setattr(self, nattr, nodes)
     def format(self, names=0):
         result = []
         bkids = self.bsources + self.bdepends + self.bimplicit
         bkidsigs = self.bsourcesigs + self.bdependsigs + self.bimplicitsigs
-        for bkid, bkidsig in zip(bkids, bkidsigs):
+        for bkid, bkidsig in izip(bkids, bkidsigs):
             result.append(str(bkid) + ': ' +
-                          string.join(bkidsig.format(names=names), ' '))
+                          ' '.join(bkidsig.format(names=names)))
         result.append('%s [%s]' % (self.bactsig, self.bact))
-        return string.join(result, '\n')
+        return '\n'.join(result)
 
 class File(Base):
     """A class for files in a file system.
@@ -1911,6 +2256,8 @@ class File(Base):
     NodeInfo = FileNodeInfo
     BuildInfo = FileBuildInfo
 
+    md5_chunksize = 64
+
     def diskcheck_match(self):
         diskcheck_match(self, self.isdir,
                         "Directory %s found where file expected.")
@@ -1922,23 +2269,25 @@ class File(Base):
 
     def Entry(self, name):
         """Create an entry node named 'name' relative to
-        the SConscript directory of this file."""
-        return self.cwd.Entry(name)
+        the directory of this file."""
+        return self.dir.Entry(name)
 
-    def Dir(self, name):
+    def Dir(self, name, create=True):
         """Create a directory node named 'name' relative to
-        the SConscript directory of this file."""
-        return self.cwd.Dir(name)
+        the directory of this file."""
+        return self.dir.Dir(name, create=create)
 
     def Dirs(self, pathlist):
         """Create a list of directories relative to the SConscript
         directory of this file."""
-        return map(lambda p, s=self: s.Dir(p), pathlist)
+        # TODO(1.5)
+        # return [self.Dir(p) for p in pathlist]
+        return [self.Dir(p) for p in pathlist]
 
     def File(self, name):
         """Create a file node named 'name' relative to
-        the SConscript directory of this file."""
-        return self.cwd.File(name)
+        the directory of this file."""
+        return self.dir.File(name)
 
     #def generate_build_dict(self):
     #    """Return an appropriate dictionary of values for building
@@ -1974,12 +2323,62 @@ class File(Base):
             return ''
         fname = self.rfile().abspath
         try:
-            r = open(fname, "rb").read()
+            contents = open(fname, "rb").read()
+        except EnvironmentError, e:
+            if not e.filename:
+                e.filename = fname
+            raise
+        return contents
+
+    try:
+        import codecs
+    except ImportError:
+        get_text_contents = get_contents
+    else:
+        # This attempts to figure out what the encoding of the text is
+        # based upon the BOM bytes, and then decodes the contents so that
+        # it's a valid python string.
+        def get_text_contents(self):
+            contents = self.get_contents()
+            # The behavior of various decode() methods and functions
+            # w.r.t. the initial BOM bytes is different for different
+            # encodings and/or Python versions.  ('utf-8' does not strip
+            # them, but has a 'utf-8-sig' which does; 'utf-16' seems to
+            # strip them; etc.)  Just side step all the complication by
+            # explicitly stripping the BOM before we decode().
+            if contents.startswith(codecs.BOM_UTF8):
+                contents = contents[len(codecs.BOM_UTF8):]
+                # TODO(2.2):  Remove when 2.3 becomes floor.
+                #contents = contents.decode('utf-8')
+                contents = my_decode(contents, 'utf-8')
+            elif contents.startswith(codecs.BOM_UTF16_LE):
+                contents = contents[len(codecs.BOM_UTF16_LE):]
+                # TODO(2.2):  Remove when 2.3 becomes floor.
+                #contents = contents.decode('utf-16-le')
+                contents = my_decode(contents, 'utf-16-le')
+            elif contents.startswith(codecs.BOM_UTF16_BE):
+                contents = contents[len(codecs.BOM_UTF16_BE):]
+                # TODO(2.2):  Remove when 2.3 becomes floor.
+                #contents = contents.decode('utf-16-be')
+                contents = my_decode(contents, 'utf-16-be')
+            return contents
+
+    def get_content_hash(self):
+        """
+        Compute and return the MD5 hash for this file.
+        """
+        if not self.rexists():
+            return SCons.Util.MD5signature('')
+        fname = self.rfile().abspath
+        try:
+            cs = SCons.Util.MD5filesignature(fname,
+                chunksize=SCons.Node.FS.File.md5_chunksize*1024)
         except EnvironmentError, e:
             if not e.filename:
                 e.filename = fname
             raise
-        return r
+        return cs
+        
 
     memoizer_counters.append(SCons.Memoize.CountValue('get_size'))
 
@@ -2020,7 +2419,8 @@ class File(Base):
         # This accomodates "chained builds" where a file that's a target
         # in one build (SConstruct file) is a source in a different build.
         # See test/chained-build.py for the use case.
-        self.dir.sconsign().store_info(self.name, self)
+        if do_store_info:
+            self.dir.sconsign().store_info(self.name, self)
 
     convert_copy_attrs = [
         'bsources',
@@ -2114,26 +2514,24 @@ class File(Base):
             try:
                 value = getattr(old_entry, attr)
             except AttributeError:
-                pass
-            else:
-                setattr(binfo, attr, value)
-                delattr(old_entry, attr)
+                continue
+            setattr(binfo, attr, value)
+            delattr(old_entry, attr)
         for attr in self.convert_sig_attrs:
             try:
                 sig_list = getattr(old_entry, attr)
             except AttributeError:
-                pass
-            else:
-                value = []
-                for sig in sig_list:
-                    ninfo = self.new_ninfo()
-                    if len(sig) == 32:
-                        ninfo.csig = sig
-                    else:
-                        ninfo.timestamp = sig
-                    value.append(ninfo)
-                setattr(binfo, attr, value)
-                delattr(old_entry, attr)
+                continue
+            value = []
+            for sig in sig_list:
+                ninfo = self.new_ninfo()
+                if len(sig) == 32:
+                    ninfo.csig = sig
+                else:
+                    ninfo.timestamp = sig
+                value.append(ninfo)
+            setattr(binfo, attr, value)
+            delattr(old_entry, attr)
         return new_entry
 
     memoizer_counters.append(SCons.Memoize.CountValue('get_stored_info'))
@@ -2146,7 +2544,7 @@ class File(Base):
 
         try:
             sconsign_entry = self.dir.sconsign().get_entry(self.name)
-        except (KeyError, OSError):
+        except (KeyError, EnvironmentError):
             import SCons.SConsign
             sconsign_entry = SCons.SConsign.SConsignEntry()
             sconsign_entry.binfo = self.new_binfo()
@@ -2171,8 +2569,8 @@ class File(Base):
         try: return binfo.bimplicit
         except AttributeError: return None
 
-#    def rel_path(self, other):
-#        return self.dir.rel_path(other)
+    def rel_path(self, other):
+        return self.dir.rel_path(other)
 
     def _get_found_includes_key(self, env, scanner, path):
         return (id(env), id(scanner), path)
@@ -2197,8 +2595,9 @@ class File(Base):
                 pass
 
         if scanner:
+            # result = [n.disambiguate() for n in scanner(self, env, path)]
             result = scanner(self, env, path)
-            result = map(lambda N: N.disambiguate(), result)
+            result = [N.disambiguate() for N in result]
         else:
             result = []
 
@@ -2211,6 +2610,22 @@ class File(Base):
         # created.
         self.dir._create()
 
+    def push_to_cache(self):
+        """Try to push the node into a cache
+        """
+        # This should get called before the Nodes' .built() method is
+        # called, which would clear the build signature if the file has
+        # a source scanner.
+        #
+        # We have to clear the local memoized values *before* we push
+        # the node to cache so that the memoization of the self.exists()
+        # return value doesn't interfere.
+        if self.nocache:
+            return
+        self.clear_memoized_values()
+        if self.exists():
+            self.get_build_env().get_CacheDir().push(self)
+
     def retrieve_from_cache(self):
         """Try to retrieve the node's content from a cache
 
@@ -2226,59 +2641,25 @@ class File(Base):
             return None
         return self.get_build_env().get_CacheDir().retrieve(self)
 
-    def built(self):
-        """
-        Called just after this node is successfully built.
-        """
-        # Push this file out to cache before the superclass Node.built()
-        # method has a chance to clear the build signature, which it
-        # will do if this file has a source scanner.
-        #
-        # We have to clear the memoized values *before* we push it to
-        # cache so that the memoization of the self.exists() return
-        # value doesn't interfere.
-        self.clear_memoized_values()
-        if self.exists():
-            self.get_build_env().get_CacheDir().push(self)
-        SCons.Node.Node.built(self)
-
     def visited(self):
         if self.exists():
             self.get_build_env().get_CacheDir().push_if_forced(self)
 
         ninfo = self.get_ninfo()
-        old = self.get_stored_info()
-
-        csig = None
-        mtime = self.get_timestamp()
-        size = self.get_size()
-
-        max_drift = self.fs.max_drift
-        if max_drift > 0:
-            if (time.time() - mtime) > max_drift:
-                try:
-                    n = old.ninfo
-                    if n.timestamp and n.csig and n.timestamp == mtime:
-                        csig = n.csig
-                except AttributeError:
-                    pass
-        elif max_drift == 0:
-            try:
-                csig = old.ninfo.csig
-            except AttributeError:
-                pass
 
+        csig = self.get_max_drift_csig()
         if csig:
             ninfo.csig = csig
 
-        ninfo.timestamp = mtime
-        ninfo.size = size
+        ninfo.timestamp = self.get_timestamp()
+        ninfo.size      = self.get_size()
 
         if not self.has_builder():
             # This is a source file, but it might have been a target file
             # in another build that included more of the DAG.  Copy
             # any build information that's stored in the .sconsign file
             # into our binfo object so it doesn't get lost.
+            old = self.get_stored_info()
             self.get_binfo().__dict__.update(old.binfo.__dict__)
 
         self.store_info()
@@ -2318,18 +2699,20 @@ class File(Base):
             scb = self.sbuilder
         except AttributeError:
             scb = self.sbuilder = self.find_src_builder()
-        return not scb is None
+        return scb is not None
 
     def alter_targets(self):
-        """Return any corresponding targets in a build directory.
+        """Return any corresponding targets in a variant directory.
         """
         if self.is_derived():
             return [], None
-        return self.fs.build_dir_target_climb(self, self.dir, [self.name])
+        return self.fs.variant_dir_target_climb(self, self.dir, [self.name])
 
     def _rmv_existing(self):
         self.clear_memoized_values()
-        Unlink(self, [], None)
+        e = Unlink(self, [], None)
+        if isinstance(e, SCons.Errors.BuildError):
+            raise e
 
     #
     # Taskmaster interface subsystem
@@ -2367,13 +2750,9 @@ class File(Base):
 
     def do_duplicate(self, src):
         self._createDir()
-        try:
-            Unlink(self, None, None)
-        except SCons.Errors.BuildError:
-            pass
-        try:
-            Link(self, src, None)
-        except SCons.Errors.BuildError, e:
+        Unlink(self, None, None)
+        e = Link(self, src, None)
+        if isinstance(e, SCons.Errors.BuildError):
             desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr)
             raise SCons.Errors.StopError, desc
         self.linked = 1
@@ -2393,8 +2772,8 @@ class File(Base):
         # Duplicate from source path if we are set up to do this.
         if self.duplicate and not self.is_derived() and not self.linked:
             src = self.srcnode()
-            if not src is self:
-                # At this point, src is meant to be copied in a build directory.
+            if src is not self:
+                # At this point, src is meant to be copied in a variant directory.
                 src = src.rfile()
                 if src.abspath != self.abspath:
                     if src.exists():
@@ -2403,7 +2782,7 @@ class File(Base):
                         # not actually occur if the -n option is being used.
                     else:
                         # The source file does not exist.  Make sure no old
-                        # copy remains in the build directory.
+                        # copy remains in the variant directory.
                         if Base.exists(self) or self.islink():
                             self.fs.unlink(self.path)
                         # Return None explicitly because the Base.exists() call
@@ -2418,6 +2797,32 @@ class File(Base):
     # SIGNATURE SUBSYSTEM
     #
 
+    def get_max_drift_csig(self):
+        """
+        Returns the content signature currently stored for this node
+        if it's been unmodified longer than the max_drift value, or the
+        max_drift value is 0.  Returns None otherwise.
+        """
+        old = self.get_stored_info()
+        mtime = self.get_timestamp()
+
+        max_drift = self.fs.max_drift
+        if max_drift > 0:
+            if (time.time() - mtime) > max_drift:
+                try:
+                    n = old.ninfo
+                    if n.timestamp and n.csig and n.timestamp == mtime:
+                        return n.csig
+                except AttributeError:
+                    pass
+        elif max_drift == 0:
+            try:
+                return old.ninfo.csig
+            except AttributeError:
+                pass
+
+        return None
+
     def get_csig(self):
         """
         Generate a node's content signature, the digested signature
@@ -2433,16 +2838,23 @@ class File(Base):
         except AttributeError:
             pass
 
-        try:
-            contents = self.get_contents()
-        except IOError:
-            # This can happen if there's actually a directory on-disk,
-            # which can be the case if they've disabled disk checks,
-            # or if an action with a File target actually happens to
-            # create a same-named directory by mistake.
-            csig = None
-        else:
-            csig = SCons.Util.MD5signature(contents)
+        csig = self.get_max_drift_csig()
+        if csig is None:
+
+            try:
+                if self.get_size() < SCons.Node.FS.File.md5_chunksize:
+                    contents = self.get_contents()
+                else:
+                    csig = self.get_content_hash()
+            except IOError:
+                # This can happen if there's actually a directory on-disk,
+                # which can be the case if they've disabled disk checks,
+                # or if an action with a File target actually happens to
+                # create a same-named directory by mistake.
+                csig = ''
+            else:
+                if not csig:
+                    csig = SCons.Util.MD5signature(contents)
 
         ninfo.csig = csig
 
@@ -2464,7 +2876,7 @@ class File(Base):
             return 1
 
     def changed_state(self, target, prev_ni):
-        return (self.state != SCons.Node.up_to_date)
+        return self.state != SCons.Node.up_to_date
 
     def changed_timestamp_then_content(self, target, prev_ni):
         if not self.changed_timestamp_match(target, prev_ni):
@@ -2511,7 +2923,9 @@ class File(Base):
                     # ...and it's even up-to-date...
                     if self._local:
                         # ...and they'd like a local copy.
-                        LocalCopy(self, r, None)
+                        e = LocalCopy(self, r, None)
+                        if isinstance(e, SCons.Errors.BuildError):
+                            raise 
                         self.store_info()
                     if T: Trace(' 1\n')
                     return 1
@@ -2540,6 +2954,19 @@ class File(Base):
                    (isinstance(node, File) or isinstance(node, Entry) \
                     or not node.is_derived()):
                         result = node
+                        # Copy over our local attributes to the repository
+                        # Node so we identify shared object files in the
+                        # repository and don't assume they're static.
+                        #
+                        # This isn't perfect; the attribute would ideally
+                        # be attached to the object in the repository in
+                        # case it was built statically in the repository
+                        # and we changed it to shared locally, but that's
+                        # rarely the case and would only occur if you
+                        # intentionally used the same suffix for both
+                        # shared and static objects anyway.  So this
+                        # should work well in practice.
+                        result.attributes = self.attributes
                         break
         self._memo['rfile'] = result
         return result
@@ -2568,8 +2995,8 @@ class File(Base):
 
         cachedir, cachefile = self.get_build_env().get_CacheDir().cachepath(self)
         if not self.exists() and cachefile and os.path.exists(cachefile):
-            contents = open(cachefile, 'rb').read()
-            self.cachedir_csig = SCons.Util.MD5signature(contents)
+            self.cachedir_csig = SCons.Util.MD5filesignature(cachefile, \
+                SCons.Node.FS.File.md5_chunksize * 1024)
         else:
             self.cachedir_csig = self.get_csig()
         return self.cachedir_csig
@@ -2583,13 +3010,15 @@ class File(Base):
         # Add the path to the cache signature, because multiple
         # targets built by the same action will all have the same
         # build signature, and we have to differentiate them somehow.
-        children =  self.children()
-        sigs = map(lambda n: n.get_cachedir_csig(), children)
+        children = self.children()
         executor = self.get_executor()
+        # sigs = [n.get_cachedir_csig() for n in children]
+        sigs = [n.get_cachedir_csig() for n in children]
         sigs.append(SCons.Util.MD5signature(executor.get_contents()))
         sigs.append(self.path)
-        self.cachesig = SCons.Util.MD5collect(sigs)
-        return self.cachesig
+        result = self.cachesig = SCons.Util.MD5collect(sigs)
+        return result
+
 
 default_fs = None
 
@@ -2610,6 +3039,41 @@ class FileFinder:
     def __init__(self):
         self._memo = {}
 
+    def filedir_lookup(self, p, fd=None):
+        """
+        A helper method for find_file() that looks up a directory for
+        a file we're trying to find.  This only creates the Dir Node if
+        it exists on-disk, since if the directory doesn't exist we know
+        we won't find any files in it...  :-)
+
+        It would be more compact to just use this as a nested function
+        with a default keyword argument (see the commented-out version
+        below), but that doesn't work unless you have nested scopes,
+        so we define it here just so this work under Python 1.5.2.
+        """
+        if fd is None:
+            fd = self.default_filedir
+        dir, name = os.path.split(fd)
+        drive, d = os.path.splitdrive(dir)
+        if not name and d[:1] in ('/', os.sep):
+            #return p.fs.get_root(drive).dir_on_disk(name)
+            return p.fs.get_root(drive)
+        if dir:
+            p = self.filedir_lookup(p, dir)
+            if not p:
+                return None
+        norm_name = _my_normcase(name)
+        try:
+            node = p.entries[norm_name]
+        except KeyError:
+            return p.dir_on_disk(name)
+        if isinstance(node, Dir):
+            return node
+        if isinstance(node, Entry):
+            node.must_be_same(Dir)
+            return node
+        return None
+
     def _find_file_key(self, filename, paths, verbose=None):
         return (filename, paths)
         
@@ -2644,32 +3108,55 @@ class FileFinder:
             except KeyError:
                 pass
 
-        if verbose:
+        if verbose and not callable(verbose):
             if not SCons.Util.is_String(verbose):
                 verbose = "find_file"
-            if not callable(verbose):
-                verbose = '  %s: ' % verbose
-                verbose = lambda s, v=verbose: sys.stdout.write(v + s)
-        else:
-            verbose = lambda x: x
+            _verbose = '  %s: ' % verbose
+            verbose = lambda s: sys.stdout.write(_verbose + s)
 
         filedir, filename = os.path.split(filename)
         if filedir:
-            def filedir_lookup(p, fd=filedir):
-                try:
-                    return p.Dir(fd)
-                except TypeError:
-                    # We tried to look up a Dir, but it seems there's
-                    # already a File (or something else) there.  No big.
-                    return None
-            paths = filter(None, map(filedir_lookup, paths))
+            # More compact code that we can't use until we drop
+            # support for Python 1.5.2:
+            #
+            #def filedir_lookup(p, fd=filedir):
+            #    """
+            #    A helper function that looks up a directory for a file
+            #    we're trying to find.  This only creates the Dir Node
+            #    if it exists on-disk, since if the directory doesn't
+            #    exist we know we won't find any files in it...  :-)
+            #    """
+            #    dir, name = os.path.split(fd)
+            #    if dir:
+            #        p = filedir_lookup(p, dir)
+            #        if not p:
+            #            return None
+            #    norm_name = _my_normcase(name)
+            #    try:
+            #        node = p.entries[norm_name]
+            #    except KeyError:
+            #        return p.dir_on_disk(name)
+            #    if isinstance(node, Dir):
+            #        return node
+            #    if isinstance(node, Entry):
+            #        node.must_be_same(Dir)
+            #        return node
+            #    if isinstance(node, Dir) or isinstance(node, Entry):
+            #        return node
+            #    return None
+            #paths = filter(None, map(filedir_lookup, paths))
+
+            self.default_filedir = filedir
+            paths = [_f for _f in map(self.filedir_lookup, paths) if _f]
 
         result = None
         for dir in paths:
-            verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
+            if verbose:
+                verbose("looking for '%s' in '%s' ...\n" % (filename, dir))
             node, d = dir.srcdir_find_file(filename)
             if node:
-                verbose("... FOUND '%s' in '%s'\n" % (filename, d))
+                if verbose:
+                    verbose("... FOUND '%s' in '%s'\n" % (filename, d))
                 result = node
                 break
 
@@ -2678,3 +3165,50 @@ class FileFinder:
         return result
 
 find_file = FileFinder().find_file
+
+
+def invalidate_node_memos(targets):
+    """
+    Invalidate the memoized values of all Nodes (files or directories)
+    that are associated with the given entries. Has been added to
+    clear the cache of nodes affected by a direct execution of an
+    action (e.g.  Delete/Copy/Chmod). Existing Node caches become
+    inconsistent if the action is run through Execute().  The argument
+    `targets` can be a single Node object or filename, or a sequence
+    of Nodes/filenames.
+    """
+    from traceback import extract_stack
+
+    # First check if the cache really needs to be flushed. Only
+    # actions run in the SConscript with Execute() seem to be
+    # affected. XXX The way to check if Execute() is in the stacktrace
+    # is a very dirty hack and should be replaced by a more sensible
+    # solution.
+    for f in extract_stack():
+        if f[2] == 'Execute' and f[0][-14:] == 'Environment.py':
+            break
+    else:
+        # Dont have to invalidate, so return
+        return
+
+    if not SCons.Util.is_List(targets):
+        targets = [targets]
+    
+    for entry in targets:
+        # If the target is a Node object, clear the cache. If it is a
+        # filename, look up potentially existing Node object first.
+        try:
+            entry.clear_memoized_values()
+        except AttributeError:
+            # Not a Node object, try to look up Node by filename.  XXX
+            # This creates Node objects even for those filenames which
+            # do not correspond to an existing Node object.
+            node = get_default_fs().Entry(entry)
+            if node:
+                node.clear_memoized_values()                        
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: