http://scons.tigris.org/issues/show_bug.cgi?id=2345
[scons.git] / src / engine / SCons / Node / FS.py
index f73161fde071b45bebaae7254187727774ea63e7..6dd5b0b78db163b4b6025e9e21ea442ca429ba27 100644 (file)
@@ -32,19 +32,71 @@ 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 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
@@ -58,6 +110,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
@@ -184,11 +255,11 @@ def set_duplicate(duplicate):
     }
 
     if not duplicate in Valid_Duplicates:
-        raise SCons.Errors.InternalError("The argument of set_duplicate "
+        raise SCons.Errors.InternalError("The argument of set_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])
 
@@ -222,8 +293,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)
@@ -303,7 +372,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()
 
 
 
@@ -337,7 +406,7 @@ def do_diskcheck_match(node, predicate, errorfmt):
     except (AttributeError, KeyError):
         pass
     if result:
-        raise TypeError, errorfmt % node.abspath
+        raise TypeError(errorfmt % node.abspath)
 
 def ignore_diskcheck_match(node, predicate, errorfmt):
     pass
@@ -389,7 +458,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]
 
 
 
@@ -427,7 +496,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):
@@ -437,7 +506,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):
@@ -482,16 +551,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)
@@ -521,37 +585,42 @@ 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__)
+        raise TypeError("Tried to lookup %s '%s' as a %s." %\
+              (self.__class__.__name__, self.path, klass.__name__))
 
     def get_dir(self):
         return self.dir
@@ -577,7 +646,7 @@ class Base(SCons.Node.Node):
             return self._memo['_save_str']
         except KeyError:
             pass
-        result = self._get_str()
+        result = sys.intern(self._get_str())
         self._memo['_save_str'] = result
         return result
 
@@ -586,7 +655,7 @@ class Base(SCons.Node.Node):
         if self.duplicate or self.is_derived():
             return self.get_path()
         srcnode = self.srcnode()
-        if srcnode.stat() is None and not self.stat() is None:
+        if srcnode.stat() is None and self.stat() is not None:
             result = self.get_path()
         else:
             result = srcnode.get_path()
@@ -601,7 +670,7 @@ class Base(SCons.Node.Node):
             # values that the underlying stat() method saved.
             try: del self._memo['stat']
             except KeyError: pass
-            if not self is srcnode:
+            if self is not srcnode:
                 try: del srcnode._memo['stat']
                 except KeyError: pass
         return result
@@ -619,7 +688,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()
@@ -636,11 +705,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):
@@ -683,8 +752,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."""
@@ -841,7 +910,7 @@ class Entry(Base):
                 self._morph()
             elif must_exist:
                 msg = "No such file or directory: '%s'" % self.abspath
-                raise SCons.Errors.UserError, msg
+                raise SCons.Errors.UserError(msg)
             else:
                 self.__class__ = File
                 self._morph()
@@ -860,11 +929,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:
@@ -877,13 +943,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
@@ -904,8 +988,8 @@ class Entry(Base):
 
     def rel_path(self, other):
         d = self.disambiguate()
-        if d.__class__ == Entry:
-            raise "rel_path() could not disambiguate File/Dir"
+        if d.__class__ is Entry:
+            raise Exception("rel_path() could not disambiguate File/Dir")
         return d.rel_path(other)
 
     def new_ninfo(self):
@@ -917,6 +1001,9 @@ class Entry(Base):
     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
@@ -1059,7 +1146,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)
@@ -1161,11 +1248,11 @@ class FS(LocalFS):
             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
@@ -1174,7 +1261,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
@@ -1186,7 +1273,7 @@ class FS(LocalFS):
         return self._lookup(name, directory, File, create)
 
     def Dir(self, name, directory = None, create = True):
-        """Lookup or create a Dir node with the specified name.  If
+        """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
@@ -1206,11 +1293,11 @@ class FS(LocalFS):
         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."
+            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'."%(variant_dir, variant_dir.srcdir)
+            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):
@@ -1239,12 +1326,12 @@ class FS(LocalFS):
                 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):
@@ -1342,7 +1429,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):
@@ -1361,8 +1448,7 @@ class Dir(Base):
         Looks up or creates a directory node named 'name' relative to
         this directory.
         """
-        dir = self.fs.Dir(name, self, create)
-        return dir
+        return self.fs.Dir(name, self, create)
 
     def File(self, name):
         """
@@ -1407,7 +1493,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
 
@@ -1423,7 +1509,7 @@ class Dir(Base):
                 fname = dir.name + os.sep + fname
             dir = dir.up()
 
-        self._memo['get_all_rdirs'] = result
+        self._memo['get_all_rdirs'] = list(result)
 
         return result
 
@@ -1445,15 +1531,15 @@ class Dir(Base):
         """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.
+        # 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..
+        # 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']
@@ -1467,11 +1553,9 @@ class Dir(Base):
                 pass
 
         if self is other:
-
             result = '.'
 
         elif not other in self.path_elements:
-
             try:
                 other_dir = other.get_dir()
             except AttributeError:
@@ -1485,15 +1569,13 @@ class Dir(Base):
                         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:])
+                         + [n.name for n in other.path_elements[i:]]
              
-            result = string.join(path_elems, os.sep)
+            result = os.sep.join(path_elems)
 
         memo_dict[other] = result
 
@@ -1538,8 +1620,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)
 
     #
     #
@@ -1556,7 +1638,9 @@ class Dir(Base):
             listDirs.append(parent)
             p = parent.up()
             if p is None:
-                raise SCons.Errors.StopError, parent.path
+                # 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()
         for dirnode in listDirs:
@@ -1577,7 +1661,7 @@ 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 variant directory.
@@ -1588,10 +1672,27 @@ class Dir(Base):
         """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
@@ -1601,7 +1702,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():
@@ -1666,9 +1767,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'))
 
@@ -1761,7 +1872,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 \
@@ -1771,7 +1885,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):
@@ -1797,8 +1911,7 @@ 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):
@@ -1835,7 +1948,8 @@ class Dir(Base):
         """
         dirname, basename = os.path.split(pathname)
         if not dirname:
-            return self._glob1(basename, ondisk, source, strings)
+            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:
@@ -1844,8 +1958,9 @@ class Dir(Base):
         for dir in list:
             r = dir._glob1(basename, ondisk, source, strings)
             if strings:
-                r = map(lambda x, d=str(dir): os.path.join(d, x), r)
+                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):
@@ -1863,55 +1978,60 @@ class Dir(Base):
         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 ('.', '..') ]
-            entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys())
-            node_names = map(lambda n, e=dir.entries: e[n].name, entry_names)
+            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:
-                    pass
-                else:
-                    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 = filter(lambda x: x[0] != '.', disk_names)
-                        disk_names = fnmatch.filter(disk_names, pattern)
-                        rep_nodes = map(dir.Entry, disk_names)
-                        #rep_nodes = [ n.disambiguate() for n in rep_nodes ]
-                        rep_nodes = map(lambda n: n.disambiguate(), rep_nodes)
-                        for node, name in zip(rep_nodes, disk_names):
-                            n = self.Entry(name)
-                            if n.__class__ != node.__class__:
-                                n.__class__ = node.__class__
-                                n._morph()
+                    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 = filter(lambda x: x[0] != '.', names)
+            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 map(lambda n, e=self.entries:  e[_my_normcase(n)], names)
+        return [self.entries[_my_normcase(n)] for n in names]
 
 class RootDir(Dir):
     """A class for the root directory of a file system.
@@ -1983,19 +2103,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.
@@ -2071,7 +2193,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:
@@ -2079,7 +2201,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
@@ -2107,23 +2229,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.
@@ -2134,6 +2255,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.")
@@ -2145,23 +2268,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, create=True):
         """Create a directory node named 'name' relative to
-        the SConscript directory of this file."""
-        return self.cwd.Dir(name, create)
+        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
@@ -2197,12 +2322,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'))
 
@@ -2243,7 +2418,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',
@@ -2337,26 +2513,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'))
@@ -2369,7 +2543,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()
@@ -2420,8 +2594,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 = []
 
@@ -2434,6 +2609,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
 
@@ -2449,22 +2640,6 @@ 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)
@@ -2523,7 +2698,7 @@ 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 variant directory.
@@ -2559,7 +2734,7 @@ class File(Base):
                     self._createDir()
                 except SCons.Errors.StopError, drive:
                     desc = "No drive `%s' for target `%s'." % (drive, self)
-                    raise SCons.Errors.StopError, desc
+                    raise SCons.Errors.StopError(desc)
 
     #
     #
@@ -2578,7 +2753,7 @@ class File(Base):
         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
+            raise SCons.Errors.StopError(desc)
         self.linked = 1
         # The Link() action may or may not have actually
         # created the file, depending on whether the -n
@@ -2596,7 +2771,7 @@ 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:
+            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:
@@ -2630,23 +2805,22 @@ class File(Base):
         old = self.get_stored_info()
         mtime = self.get_timestamp()
 
-        csig = None
         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
+                        return n.csig
                 except AttributeError:
                     pass
         elif max_drift == 0:
             try:
-                csig = old.ninfo.csig
+                return old.ninfo.csig
             except AttributeError:
                 pass
 
-        return csig
+        return None
 
     def get_csig(self):
         """
@@ -2667,7 +2841,10 @@ class File(Base):
         if csig is None:
 
             try:
-                contents = self.get_contents()
+                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,
@@ -2675,7 +2852,8 @@ class File(Base):
                 # create a same-named directory by mistake.
                 csig = ''
             else:
-                csig = SCons.Util.MD5signature(contents)
+                if not csig:
+                    csig = SCons.Util.MD5signature(contents)
 
         ninfo.csig = csig
 
@@ -2697,7 +2875,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):
@@ -2775,6 +2953,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
@@ -2803,8 +2994,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
@@ -2818,13 +3009,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
 
@@ -2861,8 +3054,9 @@ class FileFinder:
             fd = self.default_filedir
         dir, name = os.path.split(fd)
         drive, d = os.path.splitdrive(dir)
-        if d in ('/', os.sep):
-            return p.fs.get_root(drive).dir_on_disk(name)
+        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:
@@ -2872,9 +3066,10 @@ class FileFinder:
             node = p.entries[norm_name]
         except KeyError:
             return p.dir_on_disk(name)
-        # Once we move to Python 2.2 we can do:
-        #if isinstance(node, (Dir, Entry)):
-        if isinstance(node, Dir) or isinstance(node, Entry):
+        if isinstance(node, Dir):
+            return node
+        if isinstance(node, Entry):
+            node.must_be_same(Dir)
             return node
         return None
 
@@ -2912,14 +3107,11 @@ 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 = u'  %s: ' % verbose
+            verbose = lambda s: sys.stdout.write(_verbose + s)
 
         filedir, filename = os.path.split(filename)
         if filedir:
@@ -2943,22 +3135,27 @@ class FileFinder:
             #        node = p.entries[norm_name]
             #    except KeyError:
             #        return p.dir_on_disk(name)
-            #    # Once we move to Python 2.2 we can do:
-            #    #if isinstance(node, (Dir, Entry)):
+            #    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 = filter(None, map(self.filedir_lookup, paths))
+            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
 
@@ -2967,3 +3164,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: