Fix SideEffect() and BuildDir(). (Anthony Roach)
[scons.git] / src / engine / SCons / Node / FS.py
index d66b77710c3b5302175f716f85d455415c73d18c..1af603253b917cf8619061903e78e91dc313f977 100644 (file)
@@ -12,7 +12,7 @@ canonical default.
 """
 
 #
-# Copyright (c) 2001, 2002 Steven Knight
+# __COPYRIGHT__
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -36,33 +36,151 @@ canonical default.
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-import string
 import os
 import os.path
-import types
+import shutil
+import stat
+import string
+
+import SCons.Action
+import SCons.Errors
 import SCons.Node
-from UserDict import UserDict
-import sys
-from SCons.Errors import UserError
+import SCons.Util
 import SCons.Warnings
 
-try:
-    import os
-    _link = os.link
-except AttributeError:
-    import shutil
-    import stat
-    def _link(src, dest):
-        shutil.copy2(src, dest)
-        st=os.stat(src)
-        os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
-
-def file_link(src, dest):
+#
+# SCons.Action objects for interacting with the outside world.
+#
+# The Node.FS methods in this module should use these actions to
+# create and/or remove files and directories; they should *not* use
+# os.{link,symlink,unlink,mkdir}(), etc., directly.
+#
+# Using these SCons.Action objects ensures that descriptions of these
+# external activities are properly displayed, that the displays are
+# suppressed when the -s (silent) option is used, and (most importantly)
+# the actions are disabled when the the -n option is used, in which case
+# there should be *no* changes to the external file system(s)...
+#
+
+if hasattr(os, 'symlink'):
+    def _existsp(p):
+        return os.path.exists(p) or os.path.islink(p)
+else:
+    _existsp = os.path.exists
+
+def LinkFunc(target, source, env):
+    src = str(source[0])
+    dest = str(target[0])
     dir, file = os.path.split(dest)
     if dir and not os.path.isdir(dir):
         os.makedirs(dir)
-    _link(src, dest)
+    # Now actually link the files.  First try to make a hard link.  If that
+    # fails, try a symlink.  If that fails then just copy it.
+    try :
+        os.link(src, dest)
+    except (AttributeError, OSError):
+        try :
+            os.symlink(src, dest)
+        except (AttributeError, OSError):
+            shutil.copy2(src, dest)
+            st=os.stat(src)
+            os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
+    return 0
+
+Link = SCons.Action.Action(LinkFunc, None)
+
+def LocalString(target, source, env):
+    return 'Local copy of %s from %s' % (target[0], source[0])
+
+LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
+
+def UnlinkFunc(target, source, env):
+    os.unlink(target[0].path)
+    return 0
+
+Unlink = SCons.Action.Action(UnlinkFunc, None)
+
+def MkdirFunc(target, source, env):
+    os.mkdir(target[0].path)
+    return 0
+
+Mkdir = SCons.Action.Action(MkdirFunc, None)
+
+def CacheRetrieveFunc(target, source, env):
+    t = target[0]
+    cachedir, cachefile = t.cachepath()
+    if os.path.exists(cachefile):
+        shutil.copy2(cachefile, t.path)
+        st = os.stat(cachefile)
+        os.chmod(t.path, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
+        return 0
+    return 1
+
+def CacheRetrieveString(target, source, env):
+    t = target[0]
+    cachedir, cachefile = t.cachepath()
+    if os.path.exists(cachefile):
+        return "Retrieved `%s' from cache" % t.path
+    return None
+
+CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)
+
+CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)
+
+def CachePushFunc(target, source, env):
+    t = target[0]
+    cachedir, cachefile = t.cachepath()
+    if os.path.exists(cachefile):
+        # Don't bother copying it if it's already there.
+        return
+
+    if not os.path.isdir(cachedir):
+        os.mkdir(cachedir)
+
+    tempfile = cachefile+'.tmp'
+    try:
+        shutil.copy2(t.path, tempfile)
+        os.rename(tempfile, cachefile)
+        st = os.stat(t.path)
+        os.chmod(cachefile, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
+    except OSError:
+        # It's possible someone else tried writing the file at the same
+        # time we did.  Print a warning but don't stop the build, since
+        # it doesn't affect the correctness of the build.
+        SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning,
+                            "Unable to copy %s to cache. Cache file is %s"
+                                % (str(target), cachefile))
+        return
+
+CachePush = SCons.Action.Action(CachePushFunc, None)
+
+class _Null:
+    pass
+
+_null = _Null()
+
+DefaultSCCSBuilder = None
+DefaultRCSBuilder = None
+
+def get_DefaultSCCSBuilder():
+    global DefaultSCCSBuilder
+    if DefaultSCCSBuilder is None:
+        import SCons.Builder
+        import SCons.Defaults
+        DefaultSCCSBuilder = SCons.Builder.Builder(action = '$SCCSCOM',
+                                                   env = SCons.Defaults._default_env)
+    return DefaultSCCSBuilder
+
+def get_DefaultRCSBuilder():
+    global DefaultRCSBuilder
+    if DefaultRCSBuilder is None:
+        import SCons.Builder
+        import SCons.Defaults
+        DefaultRCSBuilder = SCons.Builder.Builder(action = '$RCS_COCOM',
+                                                  env = SCons.Defaults._default_env)
+    return DefaultRCSBuilder
 
+#
 class ParentOfRoot:
     """
     An instance of this class is used as the parent of the root of a
@@ -79,6 +197,7 @@ class ParentOfRoot:
         self.name=''
         self.duplicate=0
         self.srcdir=None
+        self.build_dirs=[]
         
     def is_under(self, dir):
         return 0
@@ -92,6 +211,12 @@ class ParentOfRoot:
     def get_dir(self):
         return None
 
+    def recurse_get_path(self, dir, path_elems):
+        return path_elems
+
+    def src_builder(self):
+        return _null
+
 if os.path.normcase("TeSt") == os.path.normpath("TeSt"):
     def _my_normcase(x):
         return x
@@ -99,6 +224,73 @@ else:
     def _my_normcase(x):
         return string.upper(x)
 
+class EntryProxy(SCons.Util.Proxy):
+    def __get_abspath(self):
+        entry = self.get()
+        return SCons.Util.SpecialAttrWrapper(entry.get_abspath(),
+                                             entry.name + "_abspath")
+
+    def __get_filebase(self):
+        name = self.get().name
+        return SCons.Util.SpecialAttrWrapper(os.path.splitext(name)[0],
+                                             name + "_filebase")
+
+    def __get_suffix(self):
+        entry.name = self.get().name
+        return SCons.Util.SpecialAttrWrapper(os.path.splitext(name)[1],
+                                             name + "_suffix")
+
+    def __get_file(self):
+        entry.name = self.get().name
+        return SCons.Util.SpecialAttrWrapper(name, name + "_file")
+
+    def __get_base_path(self):
+        """Return the file's directory and file name, with the
+        suffix stripped."""
+        entry = self.get()
+        return SCons.Util.SpecialAttrWrapper(os.path.splitext(entry.get_path())[0],
+                                             entry.name + "_base")
+
+    def __get_posix_path(self):
+        """Return the path with / as the path separator, regardless
+        of platform."""
+        if os.sep == '/':
+            return self
+        else:
+            entry = self.get()
+            return SCons.Util.SpecialAttrWrapper(string.replace(entry.get_path(),
+                                                                os.sep, '/'),
+                                                 entry.name + "_posix")
+
+    def __get_srcnode(self):
+        return EntryProxy(self.get().srcnode())
+
+    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."""
+        return EntryProxy(self.get().srcnode().dir)
+
+    def __get_dir(self):
+        return EntryProxy(self.get().dir)
+    
+    dictSpecialAttrs = { "base" : __get_base_path,
+                         "posix" : __get_posix_path,
+                         "srcpath" : __get_srcnode,
+                         "srcdir" : __get_srcdir,
+                         "dir" : __get_dir,
+                         "abspath" : __get_abspath,
+                         "filebase" : __get_filebase,
+                         "suffix" : __get_suffix,
+                         "file" : __get_file }
+
+    def __getattr__(self, name):
+        # This is how we implement the "special" attributes
+        # such as base, posix, srcdir, etc.
+        try:
+            return self.dictSpecialAttrs[name](self)
+        except KeyError:
+            return SCons.Util.Proxy.__getattr__(self, name)
+
 class Entry(SCons.Node.Node):
     """A generic class for file system entries.  This class is for
     when we don't know yet whether the entry being looked up is a file
@@ -122,6 +314,7 @@ class Entry(SCons.Node.Node):
 
         self.name = name
         self.fs = fs
+        self.relpath = {}
 
         assert directory, "A directory must be provided"
 
@@ -137,14 +330,29 @@ class Entry(SCons.Node.Node):
         self.cwd = None # will hold the SConscript directory for target nodes
         self.duplicate = directory.duplicate
 
+    def clear(self):
+        """Completely clear an Entry of all its cached state (so that it
+        can be re-evaluated by interfaces that do continuous integration
+        builds).
+        """
+        SCons.Node.Node.clear(self)
+        try:
+            delattr(self, '_exists')
+        except AttributeError:
+            pass
+        try:
+            delattr(self, '_rexists')
+        except AttributeError:
+            pass
+
     def get_dir(self):
         return self.dir
 
     def __str__(self):
         """A FS node's string representation is its path name."""
-        if self.duplicate or self.builder:
-            return self.path
-        return self.srcnode().path
+        if self.duplicate or self.is_derived():
+            return self.get_path()
+        return self.srcnode().get_path()
 
     def get_contents(self):
         """Fetch the contents of the entry.
@@ -166,7 +374,7 @@ class Entry(SCons.Node.Node):
         try:
             return self._exists
         except AttributeError:
-            self._exists = os.path.exists(self.abspath)
+            self._exists = _existsp(self.abspath)
             return self._exists
 
     def rexists(self):
@@ -218,6 +426,68 @@ class Entry(SCons.Node.Node):
             self._srcnode = self
             return self._srcnode
 
+    def recurse_get_path(self, dir, path_elems):
+        """Recursively build a path relative to a supplied directory
+        node."""
+        if self != dir:
+            path_elems.append(self.name)
+            path_elems = self.dir.recurse_get_path(dir, path_elems)
+        return path_elems
+
+    def get_path(self, dir=None):
+        """Return path relative to the current working directory of the
+        FS object that owns us."""
+        if not dir:
+            dir = self.fs.getcwd()
+        try:
+            return self.relpath[dir]
+        except KeyError:
+            if self == dir:
+                # Special case, return "." as the path
+                ret = '.'
+            else:
+                path_elems = self.recurse_get_path(dir, [])
+                path_elems.reverse()
+                ret = string.join(path_elems, os.sep)
+            self.relpath[dir] = ret
+            return ret
+            
+    def set_src_builder(self, builder):
+        """Set the source code builder for this node."""
+        self.sbuilder = builder
+
+    def src_builder(self):
+        """Fetch the source code builder for this node.
+
+        If there isn't one, we cache the source code builder specified
+        for the directory (which in turn will cache the value from its
+        parent directory, and so on up to the file system root).
+        """
+        try:
+            scb = self.sbuilder
+        except AttributeError:
+            scb = self.dir.src_builder()
+            self.sbuilder = scb
+        return scb
+
+    def get_abspath(self):
+        """Get the absolute path of the file."""
+        return self.abspath
+
+    def for_signature(self):
+        # Return just our name.  Even an absolute path would not work,
+        # because that can change thanks to symlinks or remapped network
+        # paths.
+        return self.name
+
+    def get_subst_proxy(self):
+        try:
+            return self._proxy
+        except AttributeError:
+            ret = EntryProxy(self)
+            self._proxy = ret
+            return ret
+
 # This is for later so we can differentiate between Entry the class and Entry
 # the method of the FS class.
 _classEntry = Entry
@@ -239,10 +509,17 @@ class FS:
             self.pathTop = path
         self.Root = {}
         self.Top = None
+        self.SConstruct_dir = None
+        self.CachePath = None
+        self.cache_force = None
+        self.cache_show = None
 
     def set_toplevel_dir(self, path):
         assert not self.Top, "You can only set the top-level path on an FS object that has not had its File, Dir, or Entry methods called yet."
         self.pathTop = path
+
+    def set_SConstruct_dir(self, dir):
+        self.SConstruct_dir = dir
         
     def __setTopLevelDir(self):
         if not self.Top:
@@ -296,7 +573,7 @@ class FS:
                 directory = self.Root[drive]
             except KeyError:
                 if not create:
-                    raise UserError
+                    raise SCons.Errors.UserError
                 dir = Dir(drive, ParentOfRoot(), self)
                 dir.path = dir.path + os.sep
                 dir.abspath = dir.abspath + os.sep
@@ -314,7 +591,7 @@ class FS:
                                               Dir)
             except KeyError:
                 if not create:
-                    raise UserError
+                    raise SCons.Errors.UserError
 
                 # look at the actual filesystem and make sure there isn't
                 # a file already there
@@ -332,7 +609,7 @@ class FS:
             ret = self.__checkClass(directory.entries[file_name], fsclass)
         except KeyError:
             if not create:
-                raise UserError
+                raise SCons.Errors.UserError
 
             # make sure we don't create File nodes when there is actually
             # a directory at that path on the disk, and vice versa
@@ -362,7 +639,7 @@ class FS:
         then the same applies.
         """
         self.__setTopLevelDir()
-        if name[0] == '#':
+        if name and name[0] == '#':
             directory = self.Top
             name = name[1:]
             if name and (name[0] == os.sep or name[0] == '/'):
@@ -374,12 +651,21 @@ class FS:
             directory = self._cwd
         return (os.path.normpath(name), directory)
 
-    def chdir(self, dir):
+    def chdir(self, dir, change_os_dir=0):
         """Change the current working directory for lookups.
+        If change_os_dir is true, we will also change the "real" cwd
+        to match.
         """
         self.__setTopLevelDir()
-        if not dir is None:
-            self._cwd = dir
+        curr=self._cwd
+        try:
+            if not dir is None:
+                self._cwd = dir
+                if change_os_dir:
+                    os.chdir(dir.abspath)
+        except:
+            self._cwd = curr
+            raise
 
     def Entry(self, name, directory = None, create = 1, klass=None):
         """Lookup or create a generic Entry node with the specified name.
@@ -436,9 +722,9 @@ class FS:
         if not isinstance(build_dir, SCons.Node.Node):
             build_dir = self.Dir(build_dir)
         if not src_dir.is_under(self.Top):
-            raise UserError, "Source directory must be under top of build tree."
+            raise SCons.Errors.UserError, "Source directory must be under top of build tree."
         if src_dir.is_under(build_dir):
-            raise UserError, "Source directory cannot be under build directory."
+            raise SCons.Errors.UserError, "Source directory cannot be under build directory."
         build_dir.link(src_dir, duplicate)
 
     def Repository(self, *dirs):
@@ -459,6 +745,13 @@ class FS:
             n = self.__doLookup(clazz, name, d)
             if n.exists():
                 return n
+            if isinstance(n, Dir):
+                # If n is a Directory that has Repositories directly
+                # attached to it, then any of those is a valid Repository
+                # path.  Return the first one that exists.
+                reps = filter(lambda x: x.exists(), n.getRepositories())
+                if len(reps):
+                    return reps[0]
             d = n.get_dir()
             name = n.name
             # Search repositories of all directories that this file is under.
@@ -473,7 +766,7 @@ class FS:
                        # This is usually the case with BuildDir().
                        # We only want to find pre-existing files.
                         if rnode.exists() and \
-                           (isinstance(rnode, Dir) or not rnode.builder):
+                           (isinstance(rnode, Dir) or not rnode.is_derived()):
                             return rnode
                     except TypeError:
                         pass # Wrong type of node.
@@ -482,7 +775,6 @@ class FS:
                 # Go up one directory
                 d = d.get_dir()
         return None
-            
 
     def Rsearchall(self, pathlist, must_exist=1, clazz=_classEntry, cwd=None):
         """Search for a list of somethings in the Repository list."""
@@ -507,18 +799,22 @@ class FS:
                     
                 d = n.get_dir()
                 name = n.name
-                # Search repositories of all directories that this file is under.
+                # Search repositories of all directories that this file
+                # is under.
                 while d:
                     for rep in d.getRepositories():
                         try:
                             rnode = self.__doLookup(clazz, name, rep)
-                            # Only find the node if it exists (or must_exist is zero)
-                            # and it is not a derived file.  If for some reason, we
-                            # are explicitly building a file IN a Repository, we don't
-                            # want it to show up in the build tree.  This is usually the
-                            # case with BuildDir().    We only want to find pre-existing files.
+                            # Only find the node if it exists (or
+                            # must_exist is zero) and it is not a
+                            # derived file.  If for some reason, we
+                            # are explicitly building a file IN a
+                            # Repository, we don't want it to show up in
+                            # the build tree.  This is usually the case
+                            # with BuildDir().  We only want to find
+                            # pre-existing files.
                             if (not must_exist or rnode.exists()) and \
-                               (not rnode.builder or isinstance(rnode, Dir)):
+                               (not rnode.is_derived() or isinstance(rnode, Dir)):
                                 ret.append(rnode)
                         except TypeError:
                             pass # Wrong type of node.
@@ -528,6 +824,27 @@ class FS:
                     d = d.get_dir()
         return ret
 
+    def CacheDir(self, path):
+        self.CachePath = path
+
+    def build_dir_target_climb(self, dir, tail):
+        """Create targets in corresponding build directories
+
+        Climb the directory tree, and look up path names
+        relative to any linked build directories we find.
+        """
+        targets = []
+        message = None
+        while dir:
+            for bd in dir.build_dirs:
+                p = apply(os.path.join, [bd.path] + tail)
+                targets.append(self.Entry(p))
+            tail = [dir.name] + tail
+            dir = dir.up()
+        if targets:
+            message = "building associated BuildDir targets: %s" % string.join(map(str, targets))
+        return targets, message
+
 # XXX TODO?
 # Annotate with the creator
 # rel_path
@@ -563,6 +880,7 @@ class Dir(Entry):
         self.cwd = self
         self.builder = 1
         self._sconsign = None
+        self.build_dirs = []
         
     def __clearRepositoryCache(self, duplicate=None):
         """Called when we change the repository(ies) for a directory.
@@ -600,21 +918,26 @@ class Dir(Entry):
     def __resetDuplicate(self, node):
         if node != self:
             node.duplicate = node.get_dir().duplicate
-        
+
+    def Entry(self, name):
+        """Create an entry node named 'name' relative to this directory."""
+        return self.fs.Entry(name, self)
+
     def Dir(self, name):
         """Create a directory node named 'name' relative to this directory."""
         return self.fs.Dir(name, self)
 
     def File(self, name):
-        """Create  file node named 'name' relatove to this directory."""
+        """Create a file node named 'name' relative to this directory."""
         return self.fs.File(name, self)
-                
+
     def link(self, srcdir, duplicate):
         """Set this directory as the build directory for the
         supplied source directory."""
         self.srcdir = srcdir
         self.duplicate = duplicate
         self.__clearRepositoryCache(duplicate)
+        srcdir.build_dirs.append(self)
 
     def getRepositories(self):
         """Returns a list of repositories for this directory."""
@@ -644,8 +967,7 @@ class Dir(Entry):
         else:
             return self.entries['..'].root()
 
-    def all_children(self, scanner):
-        #XXX --random:  randomize "dependencies?"
+    def all_children(self, scan):
         keys = filter(lambda k: k != '.' and k != '..', self.entries.keys())
         kids = map(lambda x, s=self: s.entries[x], keys)
         def c(one, two):
@@ -655,12 +977,21 @@ class Dir(Entry):
                return 1
             return 0
         kids.sort(c)
-        return kids
+        return kids + SCons.Node.Node.all_children(self, 0)
+
+    def get_actions(self):
+        """A null "builder" for directories."""
+        return []
 
     def build(self):
         """A null "builder" for directories."""
         pass
 
+    def alter_targets(self):
+        """Return any corresponding targets in a build directory.
+        """
+        return self.fs.build_dir_target_climb(self, [])
+
     def calc_signature(self, calc):
         """A directory has no signature."""
         return None
@@ -677,6 +1008,9 @@ class Dir(Entry):
         """Return a fixed "contents" value of a directory."""
         return ''
 
+    def prepare(self):
+        pass
+
     def current(self, calc):
         """If all of our children were up-to-date, then this
         directory was up-to-date, too."""
@@ -691,6 +1025,17 @@ class Dir(Entry):
         else:
             return 0
 
+    def rdir(self):
+        try:
+            return self._rdir
+        except AttributeError:
+            self._rdir = self
+            if not self.exists():
+                n = self.fs.Rsearch(self.path, clazz=Dir, cwd=self.fs.Top)
+                if n:
+                    self._rdir = n
+            return self._rdir
+
     def sconsign(self):
         """Return the .sconsign file info for this directory,
         creating it first if necessary."""
@@ -720,6 +1065,10 @@ class File(Entry):
         Entry.__init__(self, name, directory, fs)
         self._morph()
 
+    def Entry(self, name):
+        """Create an entry node named 'name' relative to
+        the SConscript directory of this file."""
+        return self.fs.Entry(name, self.cwd)
 
     def Dir(self, name):
         """Create a directory node named 'name' relative to
@@ -736,16 +1085,16 @@ class File(Entry):
         return self.fs.Rsearchall(pathlist, clazz=Dir, must_exist=0,
                                   cwd=self.cwd)
     
-    def generate_build_env(self):
-        env = SCons.Node.Node.generate_build_env(self)
-        
+    def generate_build_env(self, env):
+        """Generate an appropriate Environment to build this File."""
         return env.Override({'Dir' : self.Dir,
                              'File' : self.File,
                              'RDirs' : self.RDirs})
         
     def _morph(self):
         """Turn a file system node into a File object."""
-        self.created = 0
+        self.scanner_paths = {}
+        self.found_includes = {}
         if not hasattr(self, '_local'):
             self._local = 0
 
@@ -755,42 +1104,34 @@ class File(Entry):
     def get_contents(self):
         if not self.rexists():
             return ''
-        return open(self.rstr(), "rb").read()
+        return open(self.rfile().abspath, "rb").read()
 
     def get_timestamp(self):
         if self.rexists():
-            return os.path.getmtime(self.rstr())
+            return os.path.getmtime(self.rfile().abspath)
         else:
             return 0
 
-    def calc_signature(self, calc):
+    def calc_signature(self, calc, cache=None):
         """
         Select and calculate the appropriate build signature for a File.
 
         self - the File node
         calc - the signature calculation module
+        cache - alternate node to use for the signature cache
         returns - the signature
-
-        This method does not store the signature in the node or
-        in the .sconsign file.
         """
 
-        if self.builder:
+        if self.is_derived():
             if SCons.Sig.build_signature:
-                if not hasattr(self, 'bsig'):
-                    self.set_bsig(calc.bsig(self.rfile()))
-                return self.get_bsig()
+                return calc.bsig(self.rfile(), self)
             else:
-                if not hasattr(self, 'csig'):
-                    self.set_csig(calc.csig(self.rfile()))
-                return self.get_csig()
+                return calc.csig(self.rfile(), self)
         elif not self.rexists():
             return None
         else:
-            if not hasattr(self, 'csig'):
-                self.set_csig(calc.csig(self.rfile()))
-            return self.get_csig()
-
+            return calc.csig(self.rfile(), self)
+        
     def store_csig(self):
         self.dir.sconsign().set_csig(self.name, self.get_csig())
 
@@ -811,16 +1152,41 @@ class File(Entry):
     def get_stored_implicit(self):
         return self.dir.sconsign().get_implicit(self.name)
 
-    def get_implicit_deps(self, env, scanner, target):
-        if scanner:
-            return scanner.scan(self, env, target)
-        else:
+    def get_found_includes(self, env, scanner, target):
+        """Return the included implicit dependencies in this file.
+        Cache results so we only scan the file once regardless of
+        how many times this information is requested."""
+        if not scanner:
             return []
-        
+
+        try:
+            path = target.scanner_paths[scanner]
+        except AttributeError:
+            # The target had no scanner_paths attribute, which means
+            # it's an Alias or some other node that's not actually a
+            # file.  In that case, back off and use the path for this
+            # node itself.
+            try:
+                path = self.scanner_paths[scanner]
+            except KeyError:
+                path = scanner.path(env, self.cwd)
+                self.scanner_paths[scanner] = path
+        except KeyError:
+            path = scanner.path(env, target.cwd)
+            target.scanner_paths[scanner] = path
+
+        try:
+            includes = self.found_includes[path]
+        except KeyError:
+            includes = scanner(self, env, path)
+            self.found_includes[path] = includes
+
+        return includes
+
     def scanner_key(self):
         return os.path.splitext(self.name)[1]
 
-    def __createDir(self):
+    def _createDir(self):
         # ensure that the directories for this node are
         # created.
 
@@ -830,56 +1196,171 @@ class File(Entry):
             if parent.exists():
                 break
             listDirs.append(parent)
-            parent = parent.up()
+            p = parent.up()
+            if isinstance(p, ParentOfRoot):
+                raise SCons.Errors.StopError, parent.path
+            parent = p
         listDirs.reverse()
         for dirnode in listDirs:
             try:
-                os.mkdir(dirnode.abspath)
-                dirnode._exists = 1
+                Mkdir(dirnode, None, None)
+                # The Mkdir() action may or may not have actually
+                # created the directory, depending on whether the -n
+                # option was used or not.  Delete the _exists and
+                # _rexists attributes so they can be reevaluated.
+                if hasattr(dirnode, '_exists'):
+                    delattr(dirnode, '_exists')
+                if hasattr(dirnode, '_rexists'):
+                    delattr(dirnode, '_rexists')
             except OSError:
                 pass
 
+    def build(self):
+        """Actually build the file.
+
+        This overrides the base class build() method to check for the
+        existence of derived files in a CacheDir before going ahead and
+        building them.
+
+        This method is called from multiple threads in a parallel build,
+        so only do thread safe stuff here. Do thread unsafe stuff in
+        built().
+        """
+        b = self.is_derived()
+        if not b and not self.has_src_builder():
+            return
+        if b and self.fs.CachePath:
+            if self.fs.cache_show:
+                if CacheRetrieveSilent(self, None, None) == 0:
+                    def do_print(action, targets, sources, env, self=self):
+                        al = action.strfunction(targets, self.sources, env)
+                        if not SCons.Util.is_List(al):
+                            al = [al]
+                        for a in al:
+                            action.show(a)
+                    self._for_each_action(do_print)
+                    return
+            elif CacheRetrieve(self, None, None) == 0:
+                return
+        SCons.Node.Node.build(self)
+
     def built(self):
+        """Called just after this node is sucessfully 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.
+        if self.fs.CachePath and os.path.exists(self.path):
+            CachePush(self, None, None)
         SCons.Node.Node.built(self)
+        self.found_includes = {}
         if hasattr(self, '_exists'):
             delattr(self, '_exists')
         if hasattr(self, '_rexists'):
             delattr(self, '_rexists')
 
+    def visited(self):
+        if self.fs.CachePath and self.fs.cache_force and os.path.exists(self.path):
+            CachePush(self, None, None)
+
+    def has_src_builder(self):
+        """Return whether this Node has a source builder or not.
+
+        If this Node doesn't have an explicit source code builder, this
+        is where we figure out, on the fly, if there's a transparent
+        source code builder for it.
+
+        Note that if we found a source builder, we also set the
+        self.builder attribute, so that all of the methods that actually
+        *build* this file don't have to do anything different.
+        """
+        try:
+            scb = self.sbuilder
+        except AttributeError:
+            if self.rexists():
+                scb = None
+            else:
+                scb = self.dir.src_builder()
+                if scb is _null:
+                    scb = None
+                    dir = self.dir.path
+                    sccspath = os.path.join('SCCS', 's.' + self.name)
+                    if dir != '.':
+                        sccspath = os.path.join(dir, sccspath)
+                    if os.path.exists(sccspath):
+                        scb = get_DefaultSCCSBuilder()
+                    else:
+                        rcspath = os.path.join('RCS', self.name + ',v')
+                        if dir != '.':
+                            rcspath = os.path.join(dir, rcspath)
+                        if os.path.exists(rcspath):
+                            scb = get_DefaultRCSBuilder()
+                self.builder = scb
+            self.sbuilder = scb
+        return not scb is None
+
+    def alter_targets(self):
+        """Return any corresponding targets in a build directory.
+        """
+        if self.is_derived():
+            return [], None
+        return self.fs.build_dir_target_climb(self.dir, [self.name])
+
+    def is_pseudo_derived(self):
+        return self.has_src_builder()
+    
     def prepare(self):
         """Prepare for this file to be created."""
-        if self.exists():
-            if self.builder and not self.precious:
-                os.unlink(self.path)
-                if hasattr(self, '_exists'):
-                    delattr(self, '_exists')
-        else:
-            self.__createDir()
+
+        SCons.Node.Node.prepare(self)
+
+        if self.get_state() != SCons.Node.up_to_date:
+            if self.exists():
+                if self.is_derived() and not self.precious:
+                    try:
+                        Unlink(self, None, None)
+                    except OSError, e:
+                        raise SCons.Errors.BuildError(node = self,
+                                                      errstr = e.strerror)
+                    if hasattr(self, '_exists'):
+                        delattr(self, '_exists')
+            else:
+                try:
+                    self._createDir()
+                except SCons.Errors.StopError, drive:
+                    desc = "No drive `%s' for target `%s'." % (drive, self)
+                    raise SCons.Errors.StopError, desc
 
     def remove(self):
         """Remove this file."""
-        if os.path.exists(self.path):
+        if _existsp(self.path):
             os.unlink(self.path)
             return 1
         return None
 
     def exists(self):
         # Duplicate from source path if we are set up to do this.
-        if self.duplicate and not self.builder and not self.created:
+        if self.duplicate and not self.is_derived() and not self.linked:
             src=self.srcnode().rfile()
             if src.exists() and src.abspath != self.abspath:
+                self._createDir()
                 try:
-                    os.unlink(self.abspath)
+                    Unlink(self, None, None)
                 except OSError:
                     pass
-                self.__createDir()
-                file_link(src.abspath, self.abspath)
-                self.created = 1
-
-                # Set our exists cache accordingly
-                self._exists=1
-                self._rexists=1
-                return 1
+                try:
+                    Link(self, src, None)
+                except IOError, e:
+                    desc = "Cannot duplicate `%s' in `%s': %s." % (src, self.dir, e.strerror)
+                    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
+                # option was used or not.  Delete the _exists and
+                # _rexists attributes so they can be reevaluated.
+                if hasattr(self, '_exists'):
+                    delattr(self, '_exists')
+                if hasattr(self, '_rexists'):
+                    delattr(self, '_rexists')
         return Entry.exists(self)
 
     def current(self, calc):
@@ -893,8 +1374,7 @@ class File(Entry):
                     # ...and it's even up-to-date...
                     if self._local:
                         # ...and they'd like a local copy.
-                        print "Local copy of %s from %s" % (self.path, r.path)
-                        file_link(r.path, self.path)
+                        LocalCopy(self, r, None)
                         self.set_bsig(bsig)
                         self.store_bsig()
                     return 1
@@ -916,6 +1396,16 @@ class File(Entry):
     def rstr(self):
         return str(self.rfile())
 
+    def cachepath(self):
+        if self.fs.CachePath:
+            bsig = self.get_bsig()
+            if bsig is None:
+                raise SCons.Errors.InternalError, "cachepath(%s) found a bsig of None" % self.path
+            bsig = str(bsig)
+            subdir = string.upper(bsig[0])
+            dir = os.path.join(self.fs.CachePath, subdir)
+            return dir, os.path.join(dir, bsig)
+        return None, None
 
 default_fs = FS()
 
@@ -940,7 +1430,7 @@ def find_file(filename, paths, node_factory = default_fs.File):
         try:
             node = node_factory(filename, dir)
             # Return true of the node exists or is a derived node.
-            if node.builder or \
+            if node.is_derived() or \
                (isinstance(node, SCons.Node.FS.Entry) and node.exists()):
                 retval = node
                 break