From aaf2cbb74e00fdc89da432d18e9fe40bb7de3b9d Mon Sep 17 00:00:00 2001
From: Steven Knight <knight@baldmt.com>
Date: Tue, 6 May 2003 05:58:31 +0000
Subject: Refactor to use real Nodes for command-line attributes and eliminate
 PathList.  (Charles Crain)

---
 doc/man/scons.1                       |  12 +-
 src/CHANGES.txt                       |   3 +
 src/RELEASE.txt                       |   4 +
 src/engine/SCons/Action.py            |  36 +---
 src/engine/SCons/ActionTests.py       |  99 +++++----
 src/engine/SCons/Builder.py           |   2 +-
 src/engine/SCons/BuilderTests.py      |  18 +-
 src/engine/SCons/Environment.py       |  53 +----
 src/engine/SCons/EnvironmentTests.py  |  76 ++-----
 src/engine/SCons/Node/FS.py           |  95 +++++++--
 src/engine/SCons/Node/FSTests.py      |  70 ++++++-
 src/engine/SCons/Node/NodeTests.py    |  40 ++++
 src/engine/SCons/Node/__init__.py     |  43 +++-
 src/engine/SCons/Platform/win32.py    |   5 +-
 src/engine/SCons/Script/SConscript.py |   6 +-
 src/engine/SCons/Tool/Perforce.py     |   2 +-
 src/engine/SCons/Tool/jar.py          |   8 +-
 src/engine/SCons/Tool/javac.py        |  82 ++++----
 src/engine/SCons/Tool/linkloc.py      |   4 +-
 src/engine/SCons/Tool/mingw.py        |   4 +-
 src/engine/SCons/Tool/mslink.py       |  59 +++---
 src/engine/SCons/Tool/zip.py          |   2 +-
 src/engine/SCons/Util.py              | 366 ++++++++++++++++------------------
 src/engine/SCons/UtilTests.py         | 254 ++++++++++++++---------
 test/CacheDir.py                      |   2 +-
 test/scan-once.py                     |   2 +-
 26 files changed, 776 insertions(+), 571 deletions(-)

diff --git a/doc/man/scons.1 b/doc/man/scons.1
index 14a2e50..e6f72b9 100644
--- a/doc/man/scons.1
+++ b/doc/man/scons.1
@@ -4205,7 +4205,7 @@ takes four arguments:
 .I target
 - a list of target nodes,
 .I env
-- the construction environment.
+- the construction environment,
 .I for_signature
 - a Boolean value that specifies
 whether the generator is being called
@@ -4559,19 +4559,23 @@ may be a callable Python function
 associated with a
 construction variable in the environment.
 The function should
-take three arguments:
+take four arguments:
 .I target
 - a list of target nodes,
 .I source 
 - a list of source nodes, 
 .I env
-- the construction environment.
+- the construction environment,
+.I for_signature
+- a Boolean value that specifies
+whether the function is being called
+for generating a build signature.
 SCons will insert whatever
 the called function returns
 into the expanded string:
 
 .ES
-def foo(target, source, env):
+def foo(target, source, env, for_signature):
     return "bar"
 
 # Will expand $BAR to "bar baz"
diff --git a/src/CHANGES.txt b/src/CHANGES.txt
index 227ef59..6c6bcfb 100644
--- a/src/CHANGES.txt
+++ b/src/CHANGES.txt
@@ -52,6 +52,9 @@ RELEASE 0.14 - XXX
 
   - Pass Nodes, not strings, to Builder emitter functions.
 
+  - Refactor command-line interpolation and signature calculation
+    so we can use real Node attributes.
+
   From Steven Knight:
 
   - Add support for Java (javac and jar).
diff --git a/src/RELEASE.txt b/src/RELEASE.txt
index e8d4b37..9cad9f6 100644
--- a/src/RELEASE.txt
+++ b/src/RELEASE.txt
@@ -51,6 +51,10 @@ RELEASE 0.14 - XXX
         SetOption('num_jobs', num)
         GetOption('num_jobs')
 
+  - Callable expansions of construction variables in a command line
+    now take a fourth "for_signature" argument that is set when the
+    expansion is being called to generate a build signature.
+
   Please note the following important changes since release 0.11:
 
   - The default behavior of SCons is now to change to the directory in
diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py
index d7be127..befac1e 100644
--- a/src/engine/SCons/Action.py
+++ b/src/engine/SCons/Action.py
@@ -171,9 +171,6 @@ def _string_from_cmd_list(cmd_list):
         cl.append(arg)
     return string.join(cl)
 
-_rm = re.compile(r'\$[()]')
-_remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
-
 class CommandAction(ActionBase):
     """Class for command-execution actions."""
     def __init__(self, cmd):
@@ -183,7 +180,8 @@ class CommandAction(ActionBase):
         self.cmd_list = cmd
 
     def strfunction(self, target, source, env):
-        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
+        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env,
+                                               SCons.Util.SUBST_CMD,
                                                target, source)
         return map(_string_from_cmd_list, cmd_list)
 
@@ -228,7 +226,8 @@ class CommandAction(ActionBase):
             else:
                 raise SCons.Errors.UserError('Missing SPAWN construction variable.')
 
-        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
+        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env,
+                                               SCons.Util.SUBST_CMD,
                                                target, source)
         for cmd_line in cmd_list:
             if len(cmd_line):
@@ -259,21 +258,12 @@ class CommandAction(ActionBase):
     def get_raw_contents(self, target, source, env):
         """Return the complete contents of this action's command line.
         """
-        # We've discusssed using the real target and source names in
-        # a CommandAction's signature contents.  This would have the
-        # advantage of recompiling when a file's name changes (keeping
-        # debug info current), but it would currently break repository
-        # logic that will change the file name based on whether the
-        # files come from a repository or locally.  If we ever move to
-        # that scheme, though, here's how we'd do it:
-        #return SCons.Util.scons_subst(string.join(self.cmd_list),
-        #                              self.subst_dict(target, source, env),
-        #                              {})
         cmd = self.cmd_list
         if not SCons.Util.is_List(cmd):
             cmd = [ cmd ]
         return SCons.Util.scons_subst(string.join(map(str, cmd)),
-                                      env)
+                                      env, SCons.Util.SUBST_RAW,
+                                      target, source)
 
     def get_contents(self, target, source, env):
         """Return the signature contents of this action's command line.
@@ -281,23 +271,13 @@ class CommandAction(ActionBase):
         This strips $(-$) and everything in between the string,
         since those parts don't affect signatures.
         """
-        # We've discusssed using the real target and source names in
-        # a CommandAction's signature contents.  This would have the
-        # advantage of recompiling when a file's name changes (keeping
-        # debug info current), but it would currently break repository
-        # logic that will change the file name based on whether the
-        # files come from a repository or locally.  If we ever move to
-        # that scheme, though, here's how we'd do it:
-        #return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)),
-        #                              self.subst_dict(target, source, env),
-        #                              {},
-        #                              _remove)
         cmd = self.cmd_list
         if not SCons.Util.is_List(cmd):
             cmd = [ cmd ]
         return SCons.Util.scons_subst(string.join(map(str, cmd)),
                                       env,
-                                      _remove)
+                                      SCons.Util.SUBST_SIG,
+                                      target, source)
 
 class CommandGeneratorAction(ActionBase):
     """Class for command-generator actions."""
diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py
index 4aee748..33bf57e 100644
--- a/src/engine/SCons/ActionTests.py
+++ b/src/engine/SCons/ActionTests.py
@@ -127,6 +127,14 @@ class Environment:
         d['SOURCE'] = d['SOURCES'][0]
         return d
 
+class DummyNode:
+    def __init__(self, name):
+        self.name = name
+    def __str__(self):
+        return self.name
+    def rfile(self):
+        return self
+
 if os.name == 'java':
     python = os.path.join(sys.prefix, 'jython')
 else:
@@ -334,20 +342,23 @@ class CommandActionTestCase(unittest.TestCase):
     def test_strfunction(self):
         """Test fetching the string representation of command Actions
         """
+            
         act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE')
         s = act.strfunction([], [], Environment())
         assert s == ['xyzzy'], s
-        s = act.strfunction(['target'], ['source'], Environment())
+        s = act.strfunction([DummyNode('target')], [DummyNode('source')], Environment())
         assert s == ['xyzzy target source'], s
-        s = act.strfunction(['t1', 't2'], ['s1', 's2'], Environment())
+        s = act.strfunction([DummyNode('t1'), DummyNode('t2')],
+                            [DummyNode('s1'), DummyNode('s2')], Environment())
         assert s == ['xyzzy t1 s1'], s
 
         act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES')
         s = act.strfunction([], [], Environment())
         assert s == ['xyzzy'], s
-        s = act.strfunction(['target'], ['source'], Environment())
+        s = act.strfunction([DummyNode('target')], [DummyNode('source')], Environment())
         assert s == ['xyzzy target source'], s
-        s = act.strfunction(['t1', 't2'], ['s1', 's2'], Environment())
+        s = act.strfunction([DummyNode('t1'), DummyNode('t2')],
+                            [DummyNode('s1'), DummyNode('s2')], Environment())
         assert s == ['xyzzy t1 t2 s1 s2'], s
 
         act = SCons.Action.CommandAction(['xyzzy',
@@ -355,9 +366,10 @@ class CommandActionTestCase(unittest.TestCase):
                                           '$TARGETS', '$SOURCES'])
         s = act.strfunction([], [], Environment())
         assert s == ['xyzzy'], s
-        s = act.strfunction(['target'], ['source'], Environment())
+        s = act.strfunction([DummyNode('target')], [DummyNode('source')], Environment())
         assert s == ['xyzzy target source target source'], s
-        s = act.strfunction(['t1', 't2'], ['s1', 's2'], Environment())
+        s = act.strfunction([DummyNode('t1'), DummyNode('t2')],
+                            [DummyNode('s1'), DummyNode('s2')], Environment())
         assert s == ['xyzzy t1 s1 t1 t2 s1 s2'], s
 
     def test_execute(self):
@@ -396,7 +408,7 @@ class CommandActionTestCase(unittest.TestCase):
         cmd4 = r'%s %s %s $SOURCES' % (python, act_py, outfile)
 
         act = SCons.Action.CommandAction(cmd4)
-        r = act([], ['one', 'two'], env.Copy())
+        r = act([], [DummyNode('one'), DummyNode('two')], env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'one' 'two'\n", c
@@ -405,8 +417,10 @@ class CommandActionTestCase(unittest.TestCase):
 
         act = SCons.Action.CommandAction(cmd4)
         r = act([],
-                        source = ['three', 'four', 'five'],
-                        env = env.Copy())
+                source = [DummyNode('three'),
+                          DummyNode('four'),
+                          DummyNode('five')],
+                env = env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'three' 'four'\n", c
@@ -426,7 +440,7 @@ class CommandActionTestCase(unittest.TestCase):
         r = act(target = 'out5', source = [], env = env5)
 
         act = SCons.Action.CommandAction(cmd5)
-        r = act(target = 'out5',
+        r = act(target = DummyNode('out5'),
                         source = [],
                         env = env.Copy(ENV = {'XYZZY' : 'xyzzy',
                                               'PATH' : PATH}))
@@ -439,6 +453,8 @@ class CommandActionTestCase(unittest.TestCase):
                 self._str = str
             def __str__(self):
                 return self._str
+            def rfile(self):
+                return self
 
         cmd6 = r'%s %s %s ${TARGETS[1]} $TARGET ${SOURCES[:2]}' % (python, act_py, outfile)
 
@@ -559,8 +575,8 @@ class CommandActionTestCase(unittest.TestCase):
     def test_get_raw_contents(self):
         """Test fetching the contents of a command Action
         """
-        def CmdGen(target, source, env):
-            assert target is None, target
+        def CmdGen(target, source, env, for_signature):
+            assert for_signature
             return "%s %s" % \
                    (env["foo"], env["bar"])
 
@@ -581,47 +597,47 @@ class CommandActionTestCase(unittest.TestCase):
         # that scheme, then all of the '__t1__' and '__s6__' file names
         # in the asserts below would change to 't1' and 's6' and the
         # like.
-        t = ['t1', 't2', 't3', 't4', 't5', 't6']
-        s = ['s1', 's2', 's3', 's4', 's5', 's6']
+        t = map(DummyNode, ['t1', 't2', 't3', 't4', 't5', 't6'])
+        s = map(DummyNode, ['s1', 's2', 's3', 's4', 's5', 's6'])
         env = Environment()
 
         a = SCons.Action.CommandAction(["$TARGET"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__t1__", c
+        assert c == "t1", c
 
         a = SCons.Action.CommandAction(["$TARGETS"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__t1__ __t2__ __t3__ __t4__ __t5__ __t6__", c
+        assert c == "t1 t2 t3 t4 t5 t6", c
 
         a = SCons.Action.CommandAction(["${TARGETS[2]}"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__t3__", c
+        assert c == "t3", c
 
         a = SCons.Action.CommandAction(["${TARGETS[3:5]}"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__t4__ __t5__", c
+        assert c == "t4 t5", c
 
         a = SCons.Action.CommandAction(["$SOURCE"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__s1__", c
+        assert c == "s1", c
 
         a = SCons.Action.CommandAction(["$SOURCES"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__s1__ __s2__ __s3__ __s4__ __s5__ __s6__", c
+        assert c == "s1 s2 s3 s4 s5 s6", c
 
         a = SCons.Action.CommandAction(["${SOURCES[2]}"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__s3__", c
+        assert c == "s3", c
 
         a = SCons.Action.CommandAction(["${SOURCES[3:5]}"])
         c = a.get_raw_contents(target=t, source=s, env=env)
-        assert c == "__s4__ __s5__", c
+        assert c == "s4 s5", c
 
     def test_get_contents(self):
         """Test fetching the contents of a command Action
         """
-        def CmdGen(target, source, env):
-            assert target is None, target
+        def CmdGen(target, source, env, for_signature):
+            assert for_signature
             return "%s %s" % \
                    (env["foo"], env["bar"])
 
@@ -642,41 +658,41 @@ class CommandActionTestCase(unittest.TestCase):
         # that scheme, then all of the '__t1__' and '__s6__' file names
         # in the asserts below would change to 't1' and 's6' and the
         # like.
-        t = ['t1', 't2', 't3', 't4', 't5', 't6']
-        s = ['s1', 's2', 's3', 's4', 's5', 's6']
+        t = map(DummyNode, ['t1', 't2', 't3', 't4', 't5', 't6'])
+        s = map(DummyNode, ['s1', 's2', 's3', 's4', 's5', 's6'])
         env = Environment()
 
         a = SCons.Action.CommandAction(["$TARGET"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__t1__", c
+        assert c == "t1", c
 
         a = SCons.Action.CommandAction(["$TARGETS"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__t1__ __t2__ __t3__ __t4__ __t5__ __t6__", c
+        assert c == "t1 t2 t3 t4 t5 t6", c
 
         a = SCons.Action.CommandAction(["${TARGETS[2]}"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__t3__", c
+        assert c == "t3", c
 
         a = SCons.Action.CommandAction(["${TARGETS[3:5]}"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__t4__ __t5__", c
+        assert c == "t4 t5", c
 
         a = SCons.Action.CommandAction(["$SOURCE"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__s1__", c
+        assert c == "s1", c
 
         a = SCons.Action.CommandAction(["$SOURCES"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__s1__ __s2__ __s3__ __s4__ __s5__ __s6__", c
+        assert c == "s1 s2 s3 s4 s5 s6", c
 
         a = SCons.Action.CommandAction(["${SOURCES[2]}"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__s3__", c
+        assert c == "s3", c
 
         a = SCons.Action.CommandAction(["${SOURCES[3:5]}"])
         c = a.get_contents(target=t, source=s, env=env)
-        assert c == "__s4__ __s5__", c
+        assert c == "s4 s5", c
 
 class CommandGeneratorActionTestCase(unittest.TestCase):
 
@@ -732,6 +748,7 @@ class CommandGeneratorActionTestCase(unittest.TestCase):
                 self.t = t
             def rfile(self):
                 self.t.rfile_called = 1
+                return self
         def f3(target, source, env, for_signature):
             return ''
         c = SCons.Action.CommandGeneratorAction(f3)
@@ -745,12 +762,18 @@ class CommandGeneratorActionTestCase(unittest.TestCase):
             foo = env['foo']
             bar = env['bar']
             assert for_signature, for_signature
-            return [["guux", foo, "$(", "ignore", "$)", bar]]
+            return [["guux", foo, "$(", "$ignore", "$)", bar,
+                     '${test("$( foo $bar $)")}' ]]
+
+        def test(mystr):
+            assert mystr == "$( foo $bar $)", mystr
+            return "test"
 
         a = SCons.Action.CommandGeneratorAction(f)
         c = a.get_contents(target=[], source=[],
-                           env=Environment(foo = 'FFF', bar =  'BBB'))
-        assert c == "guux FFF BBB", c
+                           env=Environment(foo = 'FFF', bar =  'BBB',
+                                           ignore = 'foo', test=test))
+        assert c == "guux FFF BBB test", c
 
 
 class FunctionActionTestCase(unittest.TestCase):
@@ -972,7 +995,7 @@ class LazyActionTestCase(unittest.TestCase):
         """
         a = SCons.Action.Action("${FOO}")
         c = a.get_contents(target=[], source=[],
-                           env = Environment(FOO = [["This", "is", "$(", "a", "$)", "test"]]))
+                           env = Environment(FOO = [["This", "is", "$(", "$a", "$)", "test"]]))
         assert c == "This is test", c
 
 
diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py
index 19cbe27..8b0ac85 100644
--- a/src/engine/SCons/Builder.py
+++ b/src/engine/SCons/Builder.py
@@ -462,7 +462,7 @@ class MultiStepBuilder(BuilderBase):
         src_suffixes = self.src_suffixes(env)
 
         for snode in slist:
-            path, ext = SCons.Util.splitext(snode.abspath)
+            path, ext = SCons.Util.splitext(snode.get_abspath())
             if sdict.has_key(ext):
                 src_bld = sdict[ext]
                 tgt = apply(src_bld, (env, path, snode), kw)
diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py
index c71f18f..5f9a18c 100644
--- a/src/engine/SCons/BuilderTests.py
+++ b/src/engine/SCons/BuilderTests.py
@@ -248,20 +248,30 @@ class BuilderTestCase(unittest.TestCase):
         """Test returning the signature contents of a Builder
         """
 
+        class DummyNode:
+            def __init__(self, name):
+                self.name = name
+            def __str__(self):
+                return self.name
+            def rfile(self):
+                return self
+
+        target = map(DummyNode, map(lambda x: "__t%d__" % x, range(1, 7)))
+        source = map(DummyNode, map(lambda x: "__s%d__" % x, range(1, 7)))
         b1 = SCons.Builder.Builder(action = "foo ${TARGETS[5]}")
-        contents = b1.get_contents([],[],Environment())
+        contents = b1.get_contents(target,source,Environment())
         assert contents == "foo __t6__", contents
 
         b1 = SCons.Builder.Builder(action = "bar ${SOURCES[3:5]}")
-        contents = b1.get_contents([],[],Environment())
+        contents = b1.get_contents(target,source,Environment())
         assert contents == "bar __s4__ __s5__", contents
 
         b2 = SCons.Builder.Builder(action = Func)
-        contents = b2.get_contents([],[],Environment())
+        contents = b2.get_contents(target,source,Environment())
         assert contents == "\177\036\000\177\037\000d\000\000S", repr(contents)
 
         b3 = SCons.Builder.Builder(action = SCons.Action.ListAction(["foo", Func, "bar"]))
-        contents = b3.get_contents([],[],Environment())
+        contents = b3.get_contents(target,source,Environment())
         assert contents == "foo\177\036\000\177\037\000d\000\000Sbar", repr(contents)
 
     def test_node_factory(self):
diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py
index e3d29eb..f317556 100644
--- a/src/engine/SCons/Environment.py
+++ b/src/engine/SCons/Environment.py
@@ -141,34 +141,6 @@ class BuilderDict(UserDict):
         for i, v in dict.items():
             self.__setitem__(i, v)
 
-_rm = re.compile(r'\$[()]')
-
-class _lister:
-    """This class is used to implement dummy targets and sources
-    for signature calculation."""
-    def __init__(self, fmt):
-        self.fmt = fmt
-    def _format(self, index):
-        # For some reason, I originally made the fake names of
-        # the targets and sources 1-based (['__t1__, '__t2__']),
-        # not 0-based.  We preserve this behavior by adding one
-        # to the returned item names, so everyone's targets
-        # won't get recompiled if they were using an old
-        # version.
-        return self.fmt % (index + 1)
-    def __getitem__(self, index):
-        return SCons.Util.PathList([self._format(index)])[0]
-    def __getslice__(self, i, j):
-        slice = []
-        for x in range(i, j):
-            slice.append(self._format(x))
-        return SCons.Util.PathList(slice)
-    def __getattr__(self, name):
-        # If we don't find an attribute in this class, let's
-        # look in PathList.  self[0:2] returns a PathList instance
-        # via __getslice__
-        return getattr(self[0:2], name)
-
 class Environment:
     """Base class for construction Environments.  These are
     the primary objects used to communicate dependency and
@@ -442,20 +414,20 @@ class Environment:
 	trailing characters.
 	"""
         if raw:
-            regex_remove = None
+            mode = SCons.Util.SUBST_RAW
         else:
-            regex_remove = _rm
-        return SCons.Util.scons_subst(string, self, regex_remove,
+            mode = SCons.Util.SUBST_CMD
+        return SCons.Util.scons_subst(string, self, mode,
                                       target, source)
     
     def subst_list(self, string, raw=0, target=None, source=None):
         """Calls through to SCons.Util.scons_subst_list().  See
         the documentation for that function."""
         if raw:
-            regex_remove = None
+            mode = SCons.Util.SUBST_RAW
         else:
-            regex_remove = _rm
-        return SCons.Util.scons_subst_list(string, self, regex_remove,
+            mode = SCons.Util.SUBST_CMD
+        return SCons.Util.scons_subst_list(string, self, mode,
                                            target, source)
 
     def get_scanner(self, skey):
@@ -571,16 +543,3 @@ class Environment:
         if name[-len(old_suffix):] == old_suffix:
             name = name[:-len(old_suffix)]
         return os.path.join(dir, new_prefix+name+new_suffix)
-
-    def sig_dict(self):
-        """Supply a dictionary for use in computing signatures.
-
-        This fills in static TARGET, TARGETS, SOURCE and SOURCES
-        variables so that signatures stay the same every time.
-        """
-        dict = self._dict.copy()
-        dict['TARGETS'] = _lister('__t%d__')
-        dict['TARGET'] = dict['TARGETS'][0]
-        dict['SOURCES'] = _lister('__s%d__')
-        dict['SOURCE'] = dict['SOURCES'][0]
-        return dict
diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py
index e99ea0d..28ea0e5 100644
--- a/src/engine/SCons/EnvironmentTests.py
+++ b/src/engine/SCons/EnvironmentTests.py
@@ -551,34 +551,42 @@ class EnvironmentTestCase(unittest.TestCase):
 	of variables into other variables.
 	"""
 	env = Environment(AAA = 'a', BBB = 'b')
-	str = env.subst("$AAA ${AAA}A $BBBB $BBB")
-	assert str == "a aA b", str
+	mystr = env.subst("$AAA ${AAA}A $BBBB $BBB")
+	assert mystr == "a aA b", str
 
         # Changed the tests below to reflect a bug fix in
         # subst()
         env = Environment(AAA = '$BBB', BBB = 'b', BBBA = 'foo')
-	str = env.subst("$AAA ${AAA}A ${AAA}B $BBB")
-	assert str == "b bA bB b", str
+	mystr = env.subst("$AAA ${AAA}A ${AAA}B $BBB")
+	assert mystr == "b bA bB b", str
 	env = Environment(AAA = '$BBB', BBB = '$CCC', CCC = 'c')
-	str = env.subst("$AAA ${AAA}A ${AAA}B $BBB")
-	assert str == "c cA cB c", str
+	mystr = env.subst("$AAA ${AAA}A ${AAA}B $BBB")
+	assert mystr == "c cA cB c", str
 
         env = Environment(AAA = '$BBB', BBB = '$CCC', CCC = [ 'a', 'b\nc' ])
         lst = env.subst_list([ "$AAA", "B $CCC" ])
         assert lst == [ [ "a", "b" ], [ "c", "B a", "b" ], [ "c" ] ], lst
 
+        class DummyNode:
+            def __init__(self, name):
+                self.name = name
+            def __str__(self):
+                return self.name
+            def rfile(self):
+                return self
+
         # Test callables in the Environment
-        def foo(target, source, env):
-            assert target == 1, target
-            assert source == 2, source
+        def foo(target, source, env, for_signature):
+            assert str(target) == 't', target
+            assert str(source) == 's', source
             return env["FOO"]
 
         env = Environment(BAR=foo, FOO='baz')
 
-        subst = env.subst('test $BAR', target=1, source=2)
+        subst = env.subst('test $BAR', target=DummyNode('t'), source=DummyNode('s'))
         assert subst == 'test baz', subst
 
-        lst = env.subst_list('test $BAR', target=1, source=2)
+        lst = env.subst_list('test $BAR', target=DummyNode('t'), source=DummyNode('s'))
         assert lst[0][0] == 'test', lst[0][0]
         assert lst[0][1] == 'baz', lst[0][1]
 
@@ -847,52 +855,6 @@ class EnvironmentTestCase(unittest.TestCase):
                                              'PREFIX', 'SUFFIX',
                                              'LIBPREFIX', 'LIBSUFFIX')
 
-    def test_sig_dict(self):
-        """Test the sig_dict() method"""
-        d = Environment(XYZZY = 'foo').sig_dict()
-
-        assert d['XYZZY'] == 'foo'
-
-        s = str(d['TARGET'])
-        assert s == '__t1__', s
-        s = str(d['TARGET'].dir)
-        assert s == '', s
-        s = str(d['TARGETS'])
-        assert s == '__t1__ __t2__', s
-        s = str(d['TARGETS'][1])
-        assert s == '__t2__', s
-        s = str(d['TARGETS'][2])
-        assert s == '__t3__', s
-        s = str(d['TARGETS'][87])
-        assert s == '__t88__', s
-        s = str(d['TARGETS'][87].dir)
-        assert s == '', s
-        s = map(str, d['TARGETS'][3:5])
-        assert s == ['__t4__', '__t5__'], s
-        s = map(lambda x: os.path.normcase(str(x)), d['TARGETS'].abspath)
-        assert s == map(os.path.normcase, [ os.path.join(os.getcwd(), '__t1__'),
-                                            os.path.join(os.getcwd(), '__t2__') ])
-
-        s = str(d['SOURCE'])
-        assert s == '__s1__', s
-        s = str(d['SOURCE'].dir)
-        assert s == '', s
-        s = str(d['SOURCES'])
-        assert s == '__s1__ __s2__', s
-        s = str(d['SOURCES'][1])
-        assert s == '__s2__', s
-        s = str(d['SOURCES'][2])
-        assert s == '__s3__', s
-        s = str(d['SOURCES'][87])
-        assert s == '__s88__', s
-        s = str(d['SOURCES'][87].dir)
-        assert s == '', s
-        s = map(str, d['SOURCES'][3:5])
-        assert s == ['__s4__', '__s5__'], s
-        s = map(lambda x: os.path.normcase(str(x)), d['SOURCES'].abspath)
-        assert s == map(os.path.normcase, [ os.path.join(os.getcwd(), '__s1__'),
-                                            os.path.join(os.getcwd(), '__s2__') ])
-
         
 if __name__ == "__main__":
     suite = unittest.makeSuite(EnvironmentTestCase, 'test_')
diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py
index 0a016ad..e64aacc 100644
--- a/src/engine/SCons/Node/FS.py
+++ b/src/engine/SCons/Node/FS.py
@@ -41,11 +41,11 @@ import os.path
 import shutil
 import stat
 import string
-from UserDict import UserDict
 
 import SCons.Action
 import SCons.Errors
 import SCons.Node
+import SCons.Util
 import SCons.Warnings
 
 #
@@ -190,7 +190,7 @@ class ParentOfRoot:
     This class is an instance of the Null object pattern.
     """
     def __init__(self):
-        self.abspath = ''
+        self.abspath_str = ''
         self.path = ''
         self.abspath_ = ''
         self.path_ = ''
@@ -251,14 +251,14 @@ class Entry(SCons.Node.Node):
 
         assert directory, "A directory must be provided"
 
-        self.abspath = directory.abspath_ + name
+        self.abspath_str = directory.abspath_ + name
         if directory.path == '.':
             self.path = name
         else:
             self.path = directory.path_ + name
 
         self.path_ = self.path
-        self.abspath_ = self.abspath
+        self.abspath_ = self.abspath_str
         self.dir = directory
         self.cwd = None # will hold the SConscript directory for target nodes
         self.duplicate = directory.duplicate
@@ -293,11 +293,11 @@ class Entry(SCons.Node.Node):
         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."""
-        if os.path.isfile(self.abspath):
+        if os.path.isfile(self.abspath_str):
             self.__class__ = File
             self._morph()
             return File.get_contents(self)
-        if os.path.isdir(self.abspath):
+        if os.path.isdir(self.abspath_str):
             self.__class__ = Dir
             self._morph()
             return Dir.get_contents(self)
@@ -307,7 +307,7 @@ class Entry(SCons.Node.Node):
         try:
             return self._exists
         except AttributeError:
-            self._exists = _existsp(self.abspath)
+            self._exists = _existsp(self.abspath_str)
             return self._exists
 
     def rexists(self):
@@ -403,6 +403,71 @@ class Entry(SCons.Node.Node):
             self.sbuilder = scb
         return scb
 
+    def get_base_path(self):
+        """Return the file's directory and file name, with the
+        suffix stripped."""
+        return os.path.splitext(self.get_path())[0]
+
+    def get_suffix(self):
+        """Return the file's suffix."""
+        return os.path.splitext(self.get_path())[1]
+
+    def get_file_name(self):
+        """Return the file's name without the path."""
+        return self.name
+
+    def get_file_base(self):
+        """Return the file name with path and suffix stripped."""
+        return os.path.splitext(self.name)[0]
+
+    def get_posix_path(self):
+        """Return the path with / as the path separator, regardless
+        of platform."""
+        if os.sep == '/':
+            return str(self)
+        else:
+            return string.replace(self.get_path(), os.sep, '/')
+
+    def get_abspath(self):
+        """Get the absolute path of the file."""
+        return self.abspath_str
+
+    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 self.srcnode().dir
+
+    dictSpecialAttrs = { "file" : get_file_name,
+                         "base" : get_base_path,
+                         "filebase" : get_file_base,
+                         "suffix" : get_suffix,
+                         "posix" : get_posix_path,
+                         "abspath" : get_abspath,
+                         "srcpath" : srcnode,
+                         "srcdir" : get_srcdir }
+
+    def __getattr__(self, name):
+        # This is how we implement the "special" attributes
+        # such as base, suffix, basepath, etc.
+        #
+        # Note that we enclose values in a SCons.Util.Literal instance,
+        # so they will retain special characters during Environment variable
+        # substitution.
+        try:
+            attr = self.dictSpecialAttrs[name](self)
+        except KeyError:
+            raise AttributeError, '%s has no attribute: %s' % (self.__class__, name)
+        if SCons.Util.is_String(attr):
+            return SCons.Util.SpecialAttrWrapper(attr, self.name +
+                                                 "_%s" % name)
+        return attr
+
+    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
+
 # This is for later so we can differentiate between Entry the class and Entry
 # the method of the FS class.
 _classEntry = Entry
@@ -491,7 +556,7 @@ class FS:
                     raise SCons.Errors.UserError
                 dir = Dir(drive, ParentOfRoot(), self)
                 dir.path = dir.path + os.sep
-                dir.abspath = dir.abspath + os.sep
+                dir.abspath_str = dir.abspath_str + os.sep
                 self.Root[drive] = dir
                 directory = dir
             path_comp = path_comp[1:]
@@ -577,7 +642,7 @@ class FS:
             if not dir is None:
                 self._cwd = dir
                 if change_os_dir:
-                    os.chdir(dir.abspath)
+                    os.chdir(dir.abspath_str)
         except:
             self._cwd = curr
             raise
@@ -785,7 +850,7 @@ class Dir(Entry):
         node) don't use signatures for currency calculation."""
 
         self.path_ = self.path + os.sep
-        self.abspath_ = self.abspath + os.sep
+        self.abspath_ = self.abspath_str + os.sep
         self.repositories = []
         self.srcdir = None
         
@@ -886,9 +951,9 @@ class Dir(Entry):
         keys = filter(lambda k: k != '.' and k != '..', self.entries.keys())
         kids = map(lambda x, s=self: s.entries[x], keys)
         def c(one, two):
-            if one.abspath < two.abspath:
+            if one.abspath_str < two.abspath_str:
                return -1
-            if one.abspath > two.abspath:
+            if one.abspath_str > two.abspath_str:
                return 1
             return 0
         kids.sort(c)
@@ -1020,11 +1085,11 @@ class File(Entry):
     def get_contents(self):
         if not self.rexists():
             return ''
-        return open(self.rfile().abspath, "rb").read()
+        return open(self.rfile().abspath_str, "rb").read()
 
     def get_timestamp(self):
         if self.rexists():
-            return os.path.getmtime(self.rfile().abspath)
+            return os.path.getmtime(self.rfile().abspath_str)
         else:
             return 0
 
@@ -1263,7 +1328,7 @@ class File(Entry):
         # Duplicate from source path if we are set up to do this.
         if self.duplicate and not self.has_builder() and not self.linked:
             src=self.srcnode().rfile()
-            if src.exists() and src.abspath != self.abspath:
+            if src.exists() and src.abspath_str != self.abspath_str:
                 self._createDir()
                 try:
                     Unlink(self, None, None)
diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py
index f2d8dd9..209e80d 100644
--- a/src/engine/SCons/Node/FSTests.py
+++ b/src/engine/SCons/Node/FSTests.py
@@ -193,7 +193,7 @@ class BuildDirTestCase(unittest.TestCase):
         # Build path does not exist
         assert not f1.exists()
         # ...but the actual file is not there...
-        assert not os.path.exists(f1.abspath)
+        assert not os.path.exists(f1.get_abspath())
         # And duplicate=0 should also work just like a Repository
         assert f1.rexists()
         # rfile() should point to the source path
@@ -614,9 +614,9 @@ class FSTestCase(unittest.TestCase):
                 assert dir.path_ == path_, \
                        "dir.path_ %s != expected path_ %s" % \
                        (dir.path_, path_)
-                assert dir.abspath == abspath, \
+                assert dir.get_abspath() == abspath, \
                        "dir.abspath %s != expected absolute path %s" % \
-                       (dir.abspath, abspath)
+                       (dir.get_abspath(), abspath)
                 assert dir.abspath_ == abspath_, \
                        "dir.abspath_ %s != expected absolute path_ %s" % \
                        (dir.abspath_, abspath_)
@@ -1052,6 +1052,12 @@ class FSTestCase(unittest.TestCase):
 
         assert exc_caught, "Should have caught an OSError, r = " + str(r)
 
+        f = fs.Entry('foo/bar/baz')
+        assert f.for_signature() == 'baz', f.for_signature()
+        assert f.get_string(0) == os.path.normpath('foo/bar/baz'), \
+               f.get_string(0)
+        assert f.get_string(1) == 'baz', f.get_string(1)
+
 
 class RepositoryTestCase(unittest.TestCase):
     def runTest(self):
@@ -1512,7 +1518,64 @@ class clearTestCase(unittest.TestCase):
         assert not hasattr(f, '_exists')
         assert not hasattr(f, '_rexists')
 
+class SpecialAttrTestCase(unittest.TestCase):
+    def runTest(self):
+        """Test special attributes of file nodes."""
+        test=TestCmd(workdir='')
+        fs = SCons.Node.FS.FS(test.workpath(''))
 
+        f=fs.Entry('foo/bar/baz.blat')
+        assert str(f.dir) == os.path.normpath('foo/bar'), str(f.dir)
+        assert f.dir.is_literal()
+        assert f.dir.for_signature() == 'bar', f.dir.for_signature()
+        
+        assert str(f.file) == 'baz.blat', str(f.file)
+        assert f.file.is_literal()
+        assert f.file.for_signature() == 'baz.blat_file', \
+               f.file.for_signature()
+        
+        assert str(f.base) == os.path.normpath('foo/bar/baz'), str(f.base)
+        assert f.base.is_literal()
+        assert f.base.for_signature() == 'baz.blat_base', \
+               f.base.for_signature()
+        
+        assert str(f.filebase) == 'baz', str(f.filebase)
+        assert f.filebase.is_literal()
+        assert f.filebase.for_signature() == 'baz.blat_filebase', \
+               f.filebase.for_signature()
+        
+        assert str(f.suffix) == '.blat', str(f.suffix)
+        assert f.suffix.is_literal()
+        assert f.suffix.for_signature() == 'baz.blat_suffix', \
+               f.suffix.for_signature()
+        
+        assert str(f.abspath) == test.workpath('foo', 'bar', 'baz.blat'), str(f.abspath)
+        assert f.abspath.is_literal()
+        assert f.abspath.for_signature() == 'baz.blat_abspath', \
+               f.abspath.for_signature()
+        
+        assert str(f.posix) == 'foo/bar/baz.blat', str(f.posix)
+        assert f.posix.is_literal()
+        if f.posix != f:
+            assert f.posix.for_signature() == 'baz.blat_posix', \
+                   f.posix.for_signature()
+
+        fs.BuildDir('foo', 'baz')
+
+        assert str(f.srcpath) == os.path.normpath('baz/bar/baz.blat'), str(f.srcpath)
+        assert f.srcpath.is_literal()
+        assert isinstance(f.srcpath, SCons.Node.FS.Entry)
+        
+        assert str(f.srcdir) == os.path.normpath('baz/bar'), str(f.srcdir)
+        assert f.srcdir.is_literal()
+        assert isinstance(f.srcdir, SCons.Node.FS.Dir)
+
+        # And now, combinations!!!
+        assert str(f.srcpath.base) == os.path.normpath('baz/bar/baz'), str(f.srcpath.base)
+        assert str(f.srcpath.dir) == str(f.srcdir), str(f.srcpath.dir)
+        assert str(f.srcpath.posix) == 'baz/bar/baz.blat', str(f.srcpath.posix)
+        
+        
 
 if __name__ == "__main__":
     suite = unittest.TestSuite()
@@ -1527,5 +1590,6 @@ if __name__ == "__main__":
     suite.addTest(SConstruct_dirTestCase())
     suite.addTest(CacheDirTestCase())
     suite.addTest(clearTestCase())
+    suite.addTest(SpecialAttrTestCase())
     if not unittest.TextTestRunner().run(suite).wasSuccessful():
         sys.exit(1)
diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py
index 45c4af9..b188f81 100644
--- a/src/engine/SCons/Node/NodeTests.py
+++ b/src/engine/SCons/Node/NodeTests.py
@@ -294,6 +294,22 @@ class NodeTestCase(unittest.TestCase):
         c = node.builder_sig_adapter().get_contents()
         assert c == 7, c
 
+        class ListBuilder:
+            def __init__(self, targets):
+                self.tgt = targets
+            def targets(self, t):
+                return self.tgt
+            def get_contents(self, target, source, env):
+                assert target == self.tgt
+                return 8
+
+        node1 = SCons.Node.Node()
+        node2 = SCons.Node.Node()
+        node.builder_set(ListBuilder([node1, node2]))
+        node.env_set(Environment())
+        c = node.builder_sig_adapter().get_contents()
+        assert c == 8, c
+
     def test_current(self):
         """Test the default current() method
         """
@@ -747,6 +763,30 @@ class NodeTestCase(unittest.TestCase):
         n1 = MyNode("n1")
         assert n1.rstr() == 'n1', n1.rstr()
 
+    def test_abspath(self):
+        """Test the get_abspath() method."""
+        n = MyNode("foo")
+        assert n.get_abspath() == str(n), n.get_abspath()
+
+    def test_for_signature(self):
+        """Test the for_signature() method."""
+        n = MyNode("foo")
+        assert n.for_signature() == str(n), n.get_abspath()
+
+    def test_get_string(self):
+        """Test the get_string() method."""
+        class TestNode(MyNode):
+            def __init__(self, name, sig):
+                MyNode.__init__(self, name)
+                self.sig = sig
+
+            def for_signature(self):
+                return self.sig
+            
+        n = TestNode("foo", "bar")
+        assert n.get_string(0) == "foo", n.get_string(0)
+        assert n.get_string(1) == "bar", n.get_string(1)
+
     def test_arg2nodes(self):
         """Test the arg2nodes function."""
         dict = {}
diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py
index 1c23684..883d757 100644
--- a/src/engine/SCons/Node/__init__.py
+++ b/src/engine/SCons/Node/__init__.py
@@ -262,7 +262,7 @@ class Node:
             def __init__(self, node):
                 self.node = node
             def get_contents(self):
-                return self.node.builder.get_contents(self.node, self.node.sources, self.node.generate_build_env())
+                return self.node.builder.get_contents(self.node.builder.targets(self.node), self.node.sources, self.node.generate_build_env())
             def get_timestamp(self):
                 return None
         return Adapter(self)
@@ -597,6 +597,47 @@ class Node:
         else:
             return None
 
+    def get_abspath(self):
+        """
+        Return an absolute path to the Node.  This will return simply
+        str(Node) by default, but for Node types that have a concept of
+        relative path, this might return something different.
+        """
+        return str(self)
+
+    def for_signature(self):
+        """
+        Return a string representation of the Node that will always
+        be the same for this particular Node, no matter what.  This
+        is by contrast to the __str__() method, which might, for
+        instance, return a relative path for a file Node.  The purpose
+        of this method is to generate a value to be used in signature
+        calculation for the command line used to build a target, and
+        we use this method instead of str() to avoid unnecessary
+        rebuilds.  This method does not need to return something that
+        would actually work in a command line; it can return any kind of
+        nonsense, so long as it does not change.
+        """
+        return str(self)
+
+    def get_string(self, for_signature):
+        """This is a convenience function designed primarily to be
+        used in command generators (i.e., CommandGeneratorActions or
+        Environment variables that are callable), which are called
+        with a for_signature argument that is nonzero if the command
+        generator is being called to generate a signature for the
+        command line, which determines if we should rebuild or not.
+
+        Such command generators should use this method in preference
+        to str(Node) when converting a Node to a string, passing
+        in the for_signature parameter, such that we will call
+        Node.for_signature() or str(Node) properly, depending on whether
+        we are calculating a signature or actually constructing a
+        command line."""
+        if for_signature:
+            return self.for_signature()
+        return str(self)
+
 def get_children(node, parent): return node.children()
 def ignore_cycle(node, stack): pass
 def do_nothing(node, parent): pass
diff --git a/src/engine/SCons/Platform/win32.py b/src/engine/SCons/Platform/win32.py
index 39c0ba8..b36d611 100644
--- a/src/engine/SCons/Platform/win32.py
+++ b/src/engine/SCons/Platform/win32.py
@@ -54,9 +54,9 @@ class TempFileMunge:
     def __init__(self, cmd):
         self.cmd = cmd
 
-    def __call__(self, target, source, env):
+    def __call__(self, target, source, env, for_signature):
         cmd = env.subst_list(self.cmd, 0, target, source)[0]
-        if target is None or \
+        if for_signature or \
            (reduce(lambda x, y: x + len(y), cmd, 0) + len(cmd)) <= 2048:
             return self.cmd
         else:
@@ -76,7 +76,6 @@ class TempFileMunge:
             if env['SHELL'] and env['SHELL'] == 'sh':
                 native_tmp = string.replace(native_tmp, '\\', r'\\\\')
 
-
             args = map(SCons.Util.quote_spaces, cmd[1:])
             open(tmp, 'w').write(string.join(args, " ") + "\n")
             return [ cmd[0], '@' + native_tmp + '\n' + rm, native_tmp ]
diff --git a/src/engine/SCons/Script/SConscript.py b/src/engine/SCons/Script/SConscript.py
index af8cc79..5258d90 100644
--- a/src/engine/SCons/Script/SConscript.py
+++ b/src/engine/SCons/Script/SConscript.py
@@ -266,12 +266,12 @@ def SConscript(*ls, **kw):
                         # interpret the stuff within the SConscript file
                         # relative to where we are logically.
                         default_fs.chdir(ldir, change_os_dir=0)
-                        os.chdir(f.rfile().dir.abspath)
+                        os.chdir(f.rfile().dir.get_abspath())
 
                     # Append the SConscript directory to the beginning
                     # of sys.path so Python modules in the SConscript
                     # directory can be easily imported.
-                    sys.path = [ f.dir.abspath ] + sys.path
+                    sys.path = [ f.dir.get_abspath() ] + sys.path
 
                     # This is the magic line that actually reads up and
                     # executes the stuff in the SConscript file.  We
@@ -294,7 +294,7 @@ def SConscript(*ls, **kw):
                 # Repository directory.  Like above, we do this
                 # directly.
                 default_fs.chdir(frame.prev_dir, change_os_dir=0)
-                os.chdir(frame.prev_dir.rdir().abspath)
+                os.chdir(frame.prev_dir.rdir().get_abspath())
 
             results.append(frame.retval)
 
diff --git a/src/engine/SCons/Tool/Perforce.py b/src/engine/SCons/Tool/Perforce.py
index 3c574b4..3526952 100644
--- a/src/engine/SCons/Tool/Perforce.py
+++ b/src/engine/SCons/Tool/Perforce.py
@@ -70,7 +70,7 @@ def generate(env):
     # calling getcwd() for itself, which is odd.  If no PWD variable
     # is present, p4 WILL call getcwd, but this seems to cause problems
     # with good ol' Win32's tilde-mangling for long file names.
-    environ['PWD'] = SCons.Node.FS.default_fs.Dir('#').abspath
+    environ['PWD'] = SCons.Node.FS.default_fs.Dir('#').get_abspath()
 
     for var in _import_env:
         v = os.environ.get(var)
diff --git a/src/engine/SCons/Tool/jar.py b/src/engine/SCons/Tool/jar.py
index b1164b5..fa21f3c 100644
--- a/src/engine/SCons/Tool/jar.py
+++ b/src/engine/SCons/Tool/jar.py
@@ -38,15 +38,15 @@ import os.path
 
 import SCons.Builder
 
+JarBuilder = SCons.Builder.Builder(action = '$JARCOM',
+                                   source_factory = SCons.Node.FS.default_fs.Entry,
+                                   suffix = '$JARSUFFIX')
+
 def generate(env):
     """Add Builders and construction variables for jar to an Environment."""
     try:
         bld = env['BUILDERS']['Jar']
     except KeyError:
-        JarBuilder = SCons.Builder.Builder(action = '$JARCOM',
-                            source_factory = SCons.Node.FS.default_fs.Entry,
-                            suffix = '$JARSUFFIX')
-
         env['BUILDERS']['Jar'] = JarBuilder
 
     env['JAR']        = 'jar'
diff --git a/src/engine/SCons/Tool/javac.py b/src/engine/SCons/Tool/javac.py
index 331e183..8606c02 100644
--- a/src/engine/SCons/Tool/javac.py
+++ b/src/engine/SCons/Tool/javac.py
@@ -225,50 +225,50 @@ else:
         """
         return os.path.split(file)
 
+def emit_java_files(target, source, env):
+    """Create and return lists of source java files
+    and their corresponding target class files.
+    """
+    env['_JAVACLASSDIR'] = target[0]
+    env['_JAVASRCDIR'] = source[0].rdir()
+    java_suffix = env.get('JAVASUFFIX', '.java')
+    class_suffix = env.get('JAVACLASSSUFFIX', '.class')
+    
+    slist = []
+    js = _my_normcase(java_suffix)
+    def visit(arg, dirname, names, js=js, dirnode=source[0].rdir()):
+        java_files = filter(lambda n, js=js:
+                            _my_normcase(n[-len(js):]) == js,
+                            names)
+        mydir = dirnode.Dir(dirname)
+        java_paths = map(lambda f, d=mydir: d.File(f), java_files)
+        arg.extend(java_paths)
+    os.path.walk(source[0].rdir().get_abspath(), visit, slist)
+       
+    tlist = []
+    for file in slist:
+        pkg_dir, classes = parse_java(file.get_abspath())
+        if pkg_dir:
+            for c in classes:
+                tlist.append(target[0].Dir(pkg_dir).File(c+class_suffix))
+        elif classes:
+            for c in classes:
+                tlist.append(target[0].File(c+class_suffix))
+        else:
+            # This is an odd end case:  no package and no classes.
+            # Just do our best based on the source file name.
+            tlist.append(target[0].File(str(file)[:-len(java_suffix)] + class_suffix))
+            
+    return tlist, slist
+
+JavaBuilder = SCons.Builder.Builder(action = '$JAVACCOM',
+                                    emitter = emit_java_files,
+                                    target_factory = SCons.Node.FS.default_fs.Dir,
+                                    source_factory = SCons.Node.FS.default_fs.Dir)
+
 def generate(env):
     """Add Builders and construction variables for javac to an Environment."""
 
-    def emit_java_files(target, source, env):
-        """Create and return lists of source java files
-        and their corresponding target class files.
-        """
-        env['_JAVACLASSDIR'] = target[0]
-        env['_JAVASRCDIR'] = source[0].rdir()
-        java_suffix = env.get('JAVASUFFIX', '.java')
-        class_suffix = env.get('JAVACLASSSUFFIX', '.class')
-
-        slist = []
-        js = _my_normcase(java_suffix)
-        def visit(arg, dirname, names, js=js, dirnode=source[0].rdir()):
-            java_files = filter(lambda n, js=js:
-                                       _my_normcase(n[-len(js):]) == js,
-                                names)
-            mydir = dirnode.Dir(dirname)
-            java_paths = map(lambda f, d=mydir: d.File(f), java_files)
-            arg.extend(java_paths)
-        os.path.walk(source[0].rdir().abspath, visit, slist)
-       
-        tlist = []
-        for file in slist:
-            pkg_dir, classes = parse_java(file.abspath)
-            if pkg_dir:
-                for c in classes:
-                    tlist.append(target[0].Dir(pkg_dir).File(c+class_suffix))
-            elif classes:
-                for c in classes:
-                    tlist.append(target[0].File(c+class_suffix))
-            else:
-                # This is an odd end case:  no package and no classes.
-                # Just do our best based on the source file name.
-                tlist.append(target[0].File(str(file)[:-len(java_suffix)] + class_suffix))
-
-        return tlist, slist
-
-    JavaBuilder = SCons.Builder.Builder(action = '$JAVACCOM',
-                        emitter = emit_java_files,
-                        target_factory = SCons.Node.FS.default_fs.Dir,
-                        source_factory = SCons.Node.FS.default_fs.Dir)
-
     env['BUILDERS']['Java'] = JavaBuilder
 
     env['JAVAC']            = 'javac'
diff --git a/src/engine/SCons/Tool/linkloc.py b/src/engine/SCons/Tool/linkloc.py
index fc315d9..3aafe6c 100644
--- a/src/engine/SCons/Tool/linkloc.py
+++ b/src/engine/SCons/Tool/linkloc.py
@@ -65,8 +65,8 @@ class LinklocGenerator:
     def __init__(self, cmdline):
         self.cmdline = cmdline
 
-    def __call__(self, env, target, source):
-        if target is None:
+    def __call__(self, env, target, source, for_signature):
+        if for_signature:
             # Expand the contents of any linker command files recursively
             subs = 1
             strsub = env.subst(self.cmdline)
diff --git a/src/engine/SCons/Tool/mingw.py b/src/engine/SCons/Tool/mingw.py
index c89bc21..cd18bb5 100644
--- a/src/engine/SCons/Tool/mingw.py
+++ b/src/engine/SCons/Tool/mingw.py
@@ -55,10 +55,10 @@ def shlib_generator(target, source, env, for_signature):
     cmd.extend(['$SOURCES', '$_LIBDIRFLAGS', '$_LIBFLAGS'])
 
     implib = env.FindIxes(target, 'LIBPREFIX', 'LIBSUFFIX')
-    if implib: cmd.append('-Wl,--out-implib,'+str(implib))
+    if implib: cmd.append('-Wl,--out-implib,'+implib.get_string(for_signature))
 
     def_target = env.FindIxes(target, 'WIN32DEFPREFIX', 'WIN32DEFSUFFIX')
-    if def_target: cmd.append('-Wl,--output-def,'+str(def_target))
+    if def_target: cmd.append('-Wl,--output-def,'+def_target.get_string(for_signature))
 
     return [cmd]
 
diff --git a/src/engine/SCons/Tool/mslink.py b/src/engine/SCons/Tool/mslink.py
index 4bf8f0e..8aa6f09 100644
--- a/src/engine/SCons/Tool/mslink.py
+++ b/src/engine/SCons/Tool/mslink.py
@@ -44,41 +44,34 @@ import msvc
 
 from SCons.Tool.msvc import get_msdev_paths
 
-def pdbGenerator(env, target, source):
+def pdbGenerator(env, target, source, for_signature):
     if target and env.has_key('PDB') and env['PDB']:
-        return ['/PDB:%s'%target[0].File(env['PDB']), '/DEBUG']
-
-def win32ShlinkTargets(target, source, env):
-    if target:
-        listCmd = []
-        dll = env.FindIxes(target, 'SHLIBPREFIX', 'SHLIBSUFFIX')
-        if dll: listCmd.append("/out:%s"%dll)
-
-        implib = env.FindIxes(target, 'LIBPREFIX', 'LIBSUFFIX')
-        if implib: listCmd.append("/implib:%s"%implib)
-
-        return listCmd
-    else:
-        # For signature calculation
-        return '/out:$TARGET'
-
-def win32ShlinkSources(target, source, env):
-    if target:
-        listCmd = []
-
-        deffile = env.FindIxes(source, "WIN32DEFPREFIX", "WIN32DEFSUFFIX")
-        for src in source:
-            if src == deffile:
-                # Treat this source as a .def file.
-                listCmd.append("/def:%s" % src)
-            else:
-                # Just treat it as a generic source file.
-                listCmd.append(str(src))
-        return listCmd
-    else:
-        # For signature calculation
-        return "$SOURCES"
+        return ['/PDB:%s'%target[0].File(env['PDB']).get_string(for_signature),
+                '/DEBUG']
 
+def win32ShlinkTargets(target, source, env, for_signature):
+    listCmd = []
+    dll = env.FindIxes(target, 'SHLIBPREFIX', 'SHLIBSUFFIX')
+    if dll: listCmd.append("/out:%s"%dll.get_string(for_signature))
+
+    implib = env.FindIxes(target, 'LIBPREFIX', 'LIBSUFFIX')
+    if implib: listCmd.append("/implib:%s"%implib.get_string(for_signature))
+
+    return listCmd
+
+def win32ShlinkSources(target, source, env, for_signature):
+    listCmd = []
+
+    deffile = env.FindIxes(source, "WIN32DEFPREFIX", "WIN32DEFSUFFIX")
+    for src in source:
+        if src == deffile:
+            # Treat this source as a .def file.
+            listCmd.append("/def:%s" % src.get_string(for_signature))
+        else:
+            # Just treat it as a generic source file.
+            listCmd.append(src)
+    return listCmd
+    
 def win32LibEmitter(target, source, env):
     msvc.validate_vars(env)
     
diff --git a/src/engine/SCons/Tool/zip.py b/src/engine/SCons/Tool/zip.py
index 4404247..510aa6e 100644
--- a/src/engine/SCons/Tool/zip.py
+++ b/src/engine/SCons/Tool/zip.py
@@ -59,7 +59,7 @@ try:
     internal_zip = 1
 
 except ImportError:
-    zip = "$ZIP $ZIPFLAGS $( ${TARGET.abspath} $) $SOURCES"
+    zip = "$ZIP $ZIPFLAGS ${TARGET.abspath} $SOURCES"
 
     internal_zip = 0
 
diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py
index 47aa37e..b7aac31 100644
--- a/src/engine/SCons/Util.py
+++ b/src/engine/SCons/Util.py
@@ -77,6 +77,19 @@ def updrive(path):
         path = string.upper(drive) + rest
     return path
 
+if hasattr(types, 'UnicodeType'):
+    def to_String(s):
+        if isinstance(s, UserString):
+            t = type(s.data)
+        else:
+            t = type(s)
+        if t is types.UnicodeType:
+            return unicode(s)
+        else:
+            return str(s)
+else:
+    to_String = str
+
 class Literal:
     """A wrapper for a string.  If you use this object wrapped
     around a string, then it will be interpreted as literal.
@@ -91,144 +104,82 @@ class Literal:
     def is_literal(self):
         return 1
 
-class PathList(UserList.UserList):
-    """This class emulates the behavior of a list, but also implements
-    the special "path dissection" attributes we can use to find
-    suffixes, base names, etc. of the paths in the list.
-
-    One other special attribute of this class is that, by
-    overriding the __str__ and __repr__ methods, this class
-    represents itself as a space-concatenated string of
-    the list elements, as in:
-
-    >>> pl=PathList(["/foo/bar.txt", "/baz/foo.txt"])
-    >>> pl
-    '/foo/bar.txt /baz/foo.txt'
-    >>> pl.base
-    'bar foo'
-    """
+class SpecialAttrWrapper(Literal):
+    """This is a wrapper for what we call a 'Node special attribute.'
+    This is any of the attributes of a Node that we can reference from
+    Environment variable substitution, such as $TARGET.abspath or
+    $SOURCES[1].filebase.  We inherit from Literal so we can handle
+    special characters, plus we implement a for_signature method,
+    such that we can return some canonical string during signatutre
+    calculation to avoid unnecessary rebuilds."""
+
+    def __init__(self, lstr, for_signature=None):
+        """The for_signature parameter, if supplied, will be the
+        canonical string we return from for_signature().  Else
+        we will simply return lstr."""
+        Literal.__init__(self, lstr)
+        if for_signature:
+            self.forsig = for_signature
+        else:
+            self.forsig = lstr
+
+    def for_signature(self):
+        return self.forsig
+
+class CallableComposite(UserList.UserList):
+    """A simple composite callable class that, when called, will invoke all
+    of its contained callables with the same arguments."""
     def __init__(self, seq = []):
         UserList.UserList.__init__(self, seq)
 
-    def __getattr__(self, name):
-        # This is how we implement the "special" attributes
-        # such as base, suffix, basepath, etc.
-        try:
-            return self.dictSpecialAttrs[name](self)
-        except KeyError:
-            raise AttributeError, 'PathList has no attribute: %s' % name
-
-    def __splitPath(self, split_func=os.path.split):
-        """This method calls the supplied split_func on each element
-        in the contained list.  We expect split_func to return a
-        2-tuple, usually representing two elements of a split file path,
-        such as those returned by os.path.split().
-
-        We return a 2-tuple of lists, each equal in length to the contained
-        list.  The first list represents all the elements from the
-        first part of the split operation, the second represents
-        all elements from the second part."""
-        list1 = []
-        list2 = []
-        for strPath in self.data:
-            first_part, second_part = split_func(strPath)
-            list1.append(first_part)
-            list2.append(second_part)
-        # Note that we return explicit PathList() instances, not
-        # self.__class__().  This makes sure the right attributes are
-        # available even if this object is a Lister, not a PathList.
-        return (PathList(list1), PathList(list2))
-
-    def __getBasePath(self):
-        """Return the file's directory and file name, with the
-        suffix stripped."""
-        return self.__splitPath(splitext)[0]
-
-    def __getSuffix(self):
-        """Return the file's suffix."""
-        return self.__splitPath(splitext)[1]
-
-    def __getFileName(self):
-        """Return the file's name without the path."""
-        return self.__splitPath()[1]
-
-    def __getDir(self):
-        """Return the file's path."""
-        return self.__splitPath()[0]
-
-    def __getBase(self):
-        """Return the file name with path and suffix stripped."""
-        return self.__getFileName().__splitPath(splitext)[0]
-
-    def __getAbsPath(self):
-        """Return the absolute path"""
-        # Note that we return an explicit PathList() instance, not
-        # self.__class__().  This makes sure the right attributes are
-        # available even if this object is a Lister, not a PathList.
-        return PathList(map(lambda x: updrive(os.path.abspath(x)), self.data))
-
-    def __getSrcDir(self):
-        """Return the directory containing the linked
-           source file, or this file path, if not linked"""
-        sp = self.__splitPath()[0]
-        rv = []
-        for dir in sp:
-            dn = SCons.Node.FS.default_fs.Dir(str(dir))
-            if (dn == None):
-                rv = rv + ['']
-            else:
-                rv = rv + [str(dn.srcnode())]
-        return PathList(rv)
-
-    def __getSrcPath(self):
-        """Return the path to the linked source file,
-           or this file path, if not linked"""
-        rv = []
-        for dir in self.data:
-            fn = SCons.Node.FS.default_fs.File(str(dir))
-            if (fn == None):
-                rv = rv + ['']
-            else:
-                rv = rv + [str(fn.srcnode())]
-        return PathList(rv)
-
-    def __posix(self):
-        if os.sep == '/':
-            return self
-        else:
-            return PathList(map(lambda x: string.replace(x, os.sep, '/'), self.data))
-
-    dictSpecialAttrs = { "file" : __getFileName,
-                         "base" : __getBasePath,
-                         "filebase" : __getBase,
-                         "dir" : __getDir,
-                         "suffix" : __getSuffix,
-                         "abspath" : __getAbsPath,
-                         "srcpath" : __getSrcPath,
-                         "srcdir" : __getSrcDir,
-                         "posix" : __posix
-                       }
+    def __call__(self, *args, **kwargs):
+        retvals = map(lambda x, args=args, kwargs=kwargs: apply(x,
+                                                                args,
+                                                                kwargs),
+                      self.data)
+        if self.data and (len(self.data) == len(filter(callable, retvals))):
+            return self.__class__(retvals)
+        return NodeList(retvals)
+
+class NodeList(UserList.UserList):
+    """This class is almost exactly like a regular list of Nodes
+    (actually it can hold any object), with one important difference.
+    If you try to get an attribute from this list, it will return that
+    attribute from every item in the list.  For example:
+
+    >>> someList = NodeList([ '  foo  ', '  bar  ' ])
+    >>> someList.strip()
+    [ 'foo', 'bar' ]
+    """
+    def __init__(self, seq = []):
+        UserList.UserList.__init__(self, seq)
 
-    def is_literal(self):
-        return 1
+    def __nonzero__(self):
+        return len(self.data) != 0
 
     def __str__(self):
-        return string.join(self.data)
+        return string.join(map(str, self.data))
 
-    def to_String(self):
-        # Used by our variable-interpolation to interpolate a string.
-        # The interpolation doesn't use __str__() for this because then
-        # it interpolates other lists as "['x', 'y']".
-        return string.join(self.data)
-
-    def __repr__(self):
-        return repr(string.join(self.data))
+    def __getattr__(self, name):
+        if not self.data:
+            # If there is nothing in the list, then we have no attributes to
+            # pass through, so raise AttributeError for everything.
+            raise AttributeError, "NodeList has no attribute: %s" % name
+        
+        # Return a list of the attribute, gotten from every element
+        # in the list
+        attrList = map(lambda x, n=name: getattr(x, n), self.data)
+
+        # Special case.  If the attribute is callable, we do not want
+        # to return a list of callables.  Rather, we want to return a
+        # single callable that, when called, will invoke the function on
+        # all elements of this list.
+        if self.data and (len(self.data) == len(filter(callable, attrList))):
+            return CallableComposite(attrList)
+        return self.__class__(attrList)
 
-    def __getitem__(self, item):
-        # We must do this to ensure that single items returned
-        # by index access have the special attributes such as
-        # suffix and basepath.
-        return self.__class__([ UserList.UserList.__getitem__(self, item), ])
+    def is_literal(self):
+        return 1
 
 _env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[_a-zA-Z]\w*})$')
 
@@ -278,11 +229,11 @@ def quote_spaces(arg):
 #               so that we do not accidentally smush two variables
 #               together during the recursive interpolation process.
 
-_cv = re.compile(r'\$([_a-zA-Z]\w*|{[^}]*})')
+_cv = re.compile(r'\$([_a-zA-Z][\.\w]*|{[^}]*})')
 _space_sep = re.compile(r'[\t ]+(?![^{]*})')
 _newline = re.compile(r'[\r\n]+')
 
-def _convertArg(x):
+def _convertArg(x, strconv=to_String):
     """This function converts an individual argument.  If the
     argument is to be interpreted literally, with all special
     characters escaped, then we insert a special code in front
@@ -298,16 +249,16 @@ def _convertArg(x):
     if not literal:
         # escape newlines as '\0\2', '\0\1' denotes an argument split
         # Also escape double-dollar signs to mean the literal dollar sign.
-        return string.replace(_newline.sub('\0\2', to_String(x)), '$$', '\0\4')
+        return string.replace(_newline.sub('\0\2', strconv(x)), '$$', '\0\4')
     else:
         # Interpret non-string args as literals.
         # The special \0\3 code will tell us to encase this string
         # in a Literal instance when we are all done
         # Also escape out any $ signs because we don't want
         # to continue interpolating a literal.
-        return '\0\3' + string.replace(str(x), '$', '\0\4')
+        return '\0\3' + string.replace(strconv(x), '$', '\0\4')
 
-def _convert(x):
+def _convert(x, strconv = to_String):
     """This function is used to convert construction variable
     values or the value of strSubst to a string for interpolation.
     This function follows the rules outlined in the documentaion
@@ -316,12 +267,13 @@ def _convert(x):
         return ''
     elif is_String(x):
         # escape newlines as '\0\2', '\0\1' denotes an argument split
-        return _convertArg(_space_sep.sub('\0\1', x))
+        return _convertArg(_space_sep.sub('\0\1', x), strconv)
     elif is_List(x):
         # '\0\1' denotes an argument split
-        return string.join(map(_convertArg, x), '\0\1')
+        return string.join(map(lambda x, s=strconv: _convertArg(x, s), x),
+                           '\0\1')
     else:
-        return _convertArg(x)
+        return _convertArg(x, strconv)
 
 class CmdStringHolder:
     """This is a special class used to hold strings generated
@@ -414,24 +366,47 @@ def subst_dict(target, source, env):
     if not is_List(target):
         target = [target]
 
-    dict['TARGETS'] = PathList(map(os.path.normpath, map(str, target)))
+    dict['TARGETS'] = NodeList(target)
     if dict['TARGETS']:
         dict['TARGET'] = dict['TARGETS'][0]
 
-    def rstr(x):
-        try:
-            return x.rstr()
-        except AttributeError:
-            return str(x)
     if not is_List(source):
         source = [source]
-    dict['SOURCES'] = PathList(map(os.path.normpath, map(rstr, source)))
+    dict['SOURCES'] = NodeList(map(lambda x: x.rfile(), source))
     if dict['SOURCES']:
         dict['SOURCE'] = dict['SOURCES'][0]
 
     return dict
 
-def scons_subst_list(strSubst, env, remove=None, target=None,
+# Constants for the "mode" parameter to scons_subst_list() and
+# scons_subst().  SUBST_RAW gives the raw command line.  SUBST_CMD
+# gives a command line suitable for passing to a shell.  SUBST_SIG
+# gives a command line appropriate for calculating the signature
+# of a command line...if this changes, we should rebuild.
+SUBST_RAW = 0
+SUBST_CMD = 1
+SUBST_SIG = 2
+
+_rm = re.compile(r'\$[()]')
+_remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
+
+def _canonicalize(obj):
+    """Attempt to call the object's for_signature method,
+    which is expected to return a string suitable for use in calculating
+    a command line signature (i.e., it only changes when we should
+    rebuild the target).  For instance, file Nodes will report only
+    their file name (with no path), so changing Repository settings
+    will not cause a rebuild."""
+    try:
+        return obj.for_signature()
+    except AttributeError:
+        return to_String(obj)
+
+# Indexed by the SUBST_* constants above.
+_regex_remove = [ None, _rm, _remove ]
+_strconv = [ to_String, to_String, _canonicalize ]
+
+def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None,
                      source=None):
     """
     This function serves the same purpose as scons_subst(), except
@@ -457,39 +432,46 @@ def scons_subst_list(strSubst, env, remove=None, target=None,
        (e.g. file names) to contain embedded newline characters.
     """
 
+    remove = _regex_remove[mode]
+    strconv = _strconv[mode]
+    
     if target != None:
         dict = subst_dict(target, source, env)
     else:
-        dict = env.sig_dict()
+        dict = env.Dictionary()
 
     def repl(m,
              target=target,
              source=source,
              env=env,
              local_vars = dict,
-             global_vars = { "__env__" : env }):
+             global_vars = { "__env__" : env },
+             strconv=strconv,
+             sig=(mode != SUBST_CMD)):
         key = m.group(1)
         if key[0] == '{':
             key = key[1:-1]
         try:
             e = eval(key, global_vars, local_vars)
-            if callable(e):
-                # We wait to evaluate callables until the end of everything
-                # else.  For now, we instert a special escape sequence
-                # that we will look for later.
-                return '\0\5' + _convert(e(target=target,
-                                           source=source,
-                                           env=env)) + '\0\5'
-            else:
-                # The \0\5 escape code keeps us from smushing two or more
-                # variables together during recusrive substitution, i.e.
-                # foo=$bar bar=baz barbaz=blat => $foo$bar->blat (bad)
-                return "\0\5" + _convert(e) + "\0\5"
         except NameError:
             return '\0\5'
+        if callable(e):
+            # We wait to evaluate callables until the end of everything
+            # else.  For now, we instert a special escape sequence
+            # that we will look for later.
+            return '\0\5' + _convert(e(target=target,
+                                       source=source,
+                                       env=env,
+                                       for_signature=sig),
+                                     strconv) + '\0\5'
+        else:
+            # The \0\5 escape code keeps us from smushing two or more
+            # variables together during recusrive substitution, i.e.
+            # foo=$bar bar=baz barbaz=blat => $foo$bar->blat (bad)
+            return "\0\5" + _convert(e, strconv) + "\0\5"
 
     # Convert the argument to a string:
-    strSubst = _convert(strSubst)
+    strSubst = _convert(strSubst, strconv)
 
     # Do the interpolation:
     n = 1
@@ -510,7 +492,7 @@ def scons_subst_list(strSubst, env, remove=None, target=None,
     return map(lambda x: map(CmdStringHolder, filter(lambda y:y, string.split(x, '\0\1'))),
                listLines)
 
-def scons_subst(strSubst, env, remove=None, target=None,
+def scons_subst(strSubst, env, mode=SUBST_RAW, target=None,
                 source=None):
     """Recursively interpolates dictionary variables into
     the specified string, returning the expanded result.
@@ -527,32 +509,48 @@ def scons_subst(strSubst, env, remove=None, target=None,
     if target != None:
         dict = subst_dict(target, source, env)
     else:
-        dict = env.sig_dict()
+        dict = env.Dictionary()
+
+    remove = _regex_remove[mode]
+    strconv = _strconv[mode]
 
     def repl(m,
              target=target,
              source=source,
              env=env,
              local_vars = dict,
-             global_vars = { '__env__' : env }):
+             global_vars = { '__env__' : env },
+             strconv=strconv,
+             sig=(mode != SUBST_CMD)):
         key = m.group(1)
         if key[0] == '{':
             key = key[1:-1]
         try:
             e = eval(key, global_vars, local_vars)
-            if callable(e):
-                e = e(target=target, source=source, env=env)
-            if e is None:
-                s = ''
-            elif is_List(e):
-                try:
-                    s = e.to_String()
-                except AttributeError:
-                    s = string.join(map(to_String, e), ' ')
-            else:
-                s = to_String(e)
         except NameError:
+            return '\0\5'
+        if callable(e):
+            e = e(target=target, source=source, env=env, for_signature=sig)
+
+        def conv(arg, strconv=strconv):
+            literal = 0
+            try:
+                if arg.is_literal():
+                    literal = 1
+            except AttributeError:
+                pass
+            ret = strconv(arg)
+            if literal:
+                # Escape dollar signs to prevent further
+                # substitution on literals.
+                ret = string.replace(ret, '$', '\0\4')
+            return ret
+        if e is None:
             s = ''
+        elif is_List(e):
+            s = string.join(map(conv, e), ' ')
+        else:
+            s = conv(e)
         # Insert placeholders to avoid accidentally smushing
         # separate variables together.
         return "\0\5" + s + "\0\5"
@@ -563,7 +561,8 @@ def scons_subst(strSubst, env, remove=None, target=None,
         # escape double dollar signs
         strSubst = string.replace(strSubst, '$$', '\0\4')
         strSubst,n = _cv.subn(repl, strSubst)
-    # and then remove remove
+
+    # remove the remove regex
     if remove:
         strSubst = remove.sub('', strSubst)
 
@@ -615,19 +614,6 @@ def is_Dict(e):
 def is_List(e):
     return type(e) is types.ListType or isinstance(e, UserList.UserList)
 
-if hasattr(types, 'UnicodeType'):
-    def to_String(s):
-        if isinstance(s, UserString):
-            t = type(s.data)
-        else:
-            t = type(s)
-        if t is types.UnicodeType:
-            return unicode(s)
-        else:
-            return str(s)
-else:
-    to_String = str
-
 def argmunge(arg):
     return Split(arg)
 
@@ -678,7 +664,7 @@ def mapPaths(paths, dir, env=None):
                     return str(dir)
                 if os.path.isabs(path) or path[0] == '#':
                     return path
-                return dir.path_ + path
+                return str(dir) + os.sep + path
         return path
 
     if not is_List(paths):
diff --git a/src/engine/SCons/UtilTests.py b/src/engine/SCons/UtilTests.py
index b7aa478..e0543d9 100644
--- a/src/engine/SCons/UtilTests.py
+++ b/src/engine/SCons/UtilTests.py
@@ -25,17 +25,13 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 import os
 import os.path
-import re
 import string
 import sys
 import types
 import unittest
-import SCons.Node
-import SCons.Node.FS
 from SCons.Util import *
 import TestCmd
 
-
 class OutBuffer:
     def __init__(self):
         self.buffer = ""
@@ -58,33 +54,55 @@ class DummyEnv:
         dict["SOURCES"] = 'ssig'
         return dict
 
-def CmdGen1(target, source, env):
+def CmdGen1(target, source, env, for_signature):
     # Nifty trick...since Environment references are interpolated,
     # instantiate an instance of a callable class with this one,
     # which will then get evaluated.
-    assert target == 't', target
-    assert source == 's', source
-    return "${CMDGEN2('foo')}"
+    assert str(target) == 't', target
+    assert str(source) == 's', source
+    return "${CMDGEN2('foo', %d)}" % for_signature
 
 class CmdGen2:
-    def __init__(self, mystr):
+    def __init__(self, mystr, forsig):
         self.mystr = mystr
+        self.expect_for_signature = forsig
 
-    def __call__(self, target, source, env):
-        assert target == 't', target
-        assert source == 's', source
+    def __call__(self, target, source, env, for_signature):
+        assert str(target) == 't', target
+        assert str(source) == 's', source
+        assert for_signature == self.expect_for_signature, for_signature
         return [ self.mystr, env.Dictionary('BAR') ]
 
 class UtilTestCase(unittest.TestCase):
     def test_subst(self):
         """Test the subst function"""
         loc = {}
-        target = [ "./foo/bar.exe",
-                   "/bar/baz.obj",
-                   "../foo/baz.obj" ]
-        source = [ "./foo/blah.cpp",
-                   "/bar/ack.cpp",
-                   "../foo/ack.c" ]
+
+        class N:
+            """Simple node work-alike with some extra stuff for testing."""
+            def __init__(self, data):
+                self.data = os.path.normpath(data)
+
+            def __str__(self):
+                return self.data
+
+            def is_literal(self):
+                return 1
+
+            def get_stuff(self, extra):
+                return self.data + extra
+
+            def rfile(self):
+                return self
+
+            foo = 1
+        
+        target = [ N("./foo/bar.exe"),
+                   N("/bar/baz.obj"),
+                   N("../foo/baz.obj") ]
+        source = [ N("./foo/blah.cpp"),
+                   N("/bar/ack.cpp"),
+                   N("../foo/ack.c") ]
         loc['xxx'] = None
         loc['zero'] = 0
         loc['one'] = 1
@@ -122,58 +140,33 @@ class UtilTestCase(unittest.TestCase):
                              target=target, source=source)
         assert newcom == cvt("test foo/bar.exe[0]")
 
-        newcom = scons_subst("test ${TARGET.file}", env,
-                             target=target, source=source)
-        assert newcom == cvt("test bar.exe")
-
-        newcom = scons_subst("test ${TARGET.filebase}", env,
+        newcom = scons_subst("test $TARGETS.foo", env,
                              target=target, source=source)
-        assert newcom == cvt("test bar")
+        assert newcom == "test 1 1 1", newcom
 
-        newcom = scons_subst("test ${TARGET.suffix}", env,
+        newcom = scons_subst("test ${SOURCES[0:2].foo}", env,
                              target=target, source=source)
-        assert newcom == cvt("test .exe")
+        assert newcom == "test 1 1", newcom
 
-        newcom = scons_subst("test ${TARGET.base}", env,
+        newcom = scons_subst("test $SOURCE.foo", env,
                              target=target, source=source)
-        assert newcom == cvt("test foo/bar")
+        assert newcom == "test 1", newcom
 
-        newcom = scons_subst("test ${TARGET.dir}", env,
+        newcom = scons_subst("test ${TARGET.get_stuff('blah')}", env,
                              target=target, source=source)
-        assert newcom == cvt("test foo")
+        assert newcom == cvt("test foo/bar.exeblah"), newcom
 
-        newcom = scons_subst("test ${TARGET.abspath}", env,
+        newcom = scons_subst("test ${SOURCES.get_stuff('blah')}", env,
                              target=target, source=source)
-        assert newcom == cvt("test %s/foo/bar.exe"%SCons.Util.updrive(os.getcwd())), newcom
+        assert newcom == cvt("test foo/blah.cppblah /bar/ack.cppblah ../foo/ack.cblah"), newcom
 
-        newcom = scons_subst("test ${SOURCES.abspath}", env,
+        newcom = scons_subst("test ${SOURCES[0:2].get_stuff('blah')}", env,
                              target=target, source=source)
-        assert newcom == cvt("test %s/foo/blah.cpp %s %s/foo/ack.c"%(SCons.Util.updrive(os.getcwd()),
-                                                                     SCons.Util.updrive(os.path.abspath(os.path.normpath("/bar/ack.cpp"))),
-                                                                     SCons.Util.updrive(os.path.normpath(os.getcwd()+"/..")))), newcom
+        assert newcom == cvt("test foo/blah.cppblah /bar/ack.cppblah"), newcom
 
-        newcom = scons_subst("test ${SOURCE.abspath}", env,
+        newcom = scons_subst("test ${SOURCES[0:2].get_stuff('blah')}", env,
                              target=target, source=source)
-        assert newcom == cvt("test %s/foo/blah.cpp"%SCons.Util.updrive(os.getcwd())), newcom
-
-        # Note that we don't use the cvt() helper function here,
-        # because we're testing that the .posix attribute does its own
-        # conversion of the path name backslashes to slashes.
-        newcom = scons_subst("test ${TARGET.posix} ${SOURCE.posix}", env,
-                             target=target, source=source)
-        assert newcom == "test foo/bar.exe foo/blah.cpp", newcom
-
-        SCons.Node.FS.default_fs.BuildDir("#baz","#foo")
-
-        newcom = scons_subst("test ${SOURCE.srcdir}", env,
-                             target=target, source=['baz/bar.c'])
-
-        assert newcom == cvt("test foo"), newcom
-
-        newcom = scons_subst("test ${SOURCE.srcpath}", env,
-                             target=target, source=['baz/bar.c'])
-
-        assert newcom == cvt("test foo/bar.c"), newcom
+        assert newcom == cvt("test foo/blah.cppblah /bar/ack.cppblah"), newcom
 
         newcom = scons_subst("test $xxx", env)
         assert newcom == cvt("test"), newcom
@@ -184,10 +177,10 @@ class UtilTestCase(unittest.TestCase):
         newcom = scons_subst("test $( $xxx $)", env)
         assert newcom == cvt("test $( $)"), newcom
 
-        newcom = scons_subst("test $($xxx$)", env, re.compile('\$[()]'))
+        newcom = scons_subst("test $($xxx$)", env, mode=SUBST_SIG)
         assert newcom == cvt("test"), newcom
 
-        newcom = scons_subst("test $( $xxx $)", env, re.compile('\$[()]'))
+        newcom = scons_subst("test $( $xxx $)", env, mode=SUBST_SIG)
         assert newcom == cvt("test"), newcom
 
         newcom = scons_subst("test $zero", env)
@@ -196,11 +189,8 @@ class UtilTestCase(unittest.TestCase):
         newcom = scons_subst("test $one", env)
         assert newcom == cvt("test 1"), newcom
 
-        newcom = scons_subst("test aXbXcXd", env, re.compile('X'))
-        assert newcom == cvt("test abcd"), newcom
-
         newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS",
-                             env, target='t', source='s')
+                             env, target=N('t'), source=N('s'))
         assert newcom == cvt("test foo baz s t"), newcom
 
         # Test against a former bug in scons_subst_list()
@@ -217,6 +207,23 @@ class UtilTestCase(unittest.TestCase):
         newcom = scons_subst("$$FOO$BAZ", DummyEnv(glob))
         assert newcom == "$FOOBLAT", newcom
 
+        class TestLiteral:
+            def __init__(self, literal):
+                self.literal = literal
+
+            def __str__(self):
+                return self.literal
+
+            def is_literal(self):
+                return 1
+
+        # Test that a literal will stop dollar-sign substitution
+        glob = { "FOO" : "BAR",
+                 "BAZ" : TestLiteral("$FOO"),
+                 "BAR" : "$FOO" }
+        newcom = scons_subst("$FOO $BAZ $BAR", DummyEnv(glob))
+        assert newcom == "BAR $FOO BAR", newcom
+
     def test_splitext(self):
         assert splitext('foo') == ('foo','')
         assert splitext('foo.bar') == ('foo','.bar')
@@ -227,19 +234,21 @@ class UtilTestCase(unittest.TestCase):
 
         class Node:
             def __init__(self, name):
-                self.name = name
+                self.name = os.path.normpath(name)
             def __str__(self):
                 return self.name
             def is_literal(self):
                 return 1
+            def rfile(self):
+                return self
         
         loc = {}
-        target = [ "./foo/bar.exe",
-                   "/bar/baz with spaces.obj",
-                   "../foo/baz.obj" ]
-        source = [ "./foo/blah with spaces.cpp",
-                   "/bar/ack.cpp",
-                   "../foo/ack.c" ]
+        target = [ Node("./foo/bar.exe"),
+                   Node("/bar/baz with spaces.obj"),
+                   Node("../foo/baz.obj") ]
+        source = [ Node("./foo/blah with spaces.cpp"),
+                   Node("/bar/ack.cpp"),
+                   Node("../foo/ack.c") ]
         loc['xxx'] = None
         loc['NEWLINE'] = 'before\nafter'
 
@@ -307,7 +316,7 @@ class UtilTestCase(unittest.TestCase):
 
         # Test interpolating a callable.
         cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES", env,
-                                    target='t', source='s')
+                                    target=Node('t'), source=Node('s'))
         assert len(cmd_list) == 1, len(cmd_list)
         assert cmd_list[0][0] == 'testing', cmd_list[0][0]
         assert cmd_list[0][1] == 'foo', cmd_list[0][1]
@@ -335,8 +344,8 @@ class UtilTestCase(unittest.TestCase):
             return '**' + foo + '**'
         def quote_func(foo):
             return foo
-        glob = { "FOO" : PathList([ 'foo\nwith\nnewlines',
-                                    'bar\nwith\nnewlines' ]) }
+        glob = { "FOO" : [ Literal('foo\nwith\nnewlines'),
+                           Literal('bar\nwith\nnewlines') ] }
         cmd_list = scons_subst_list("$FOO", DummyEnv(glob))
         assert cmd_list[0][0] == 'foo\nwith\nnewlines', cmd_list[0][0]
         cmd_list[0][0].escape(escape_func)
@@ -595,11 +604,41 @@ class UtilTestCase(unittest.TestCase):
         assert cmd_list[0] == 'BAZ', cmd_list[0]
         assert cmd_list[1] == '**$BAR**', cmd_list[1]
 
+    def test_SpecialAttrWrapper(self):
+        """Test the SpecialAttrWrapper() function."""
+        input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ]
+        
+        def escape_func(cmd):
+            return '**' + cmd + '**'
+
+        
+        cmd_list = scons_subst_list(input_list,
+                                    DummyEnv({ 'FOO' : 'BAZ',
+                                               'BAR' : 'BLAT' }))
+        map(lambda x, e=escape_func: x.escape(e), cmd_list[0])
+        cmd_list = map(str, cmd_list[0])
+        assert cmd_list[0] == 'BAZ', cmd_list[0]
+        assert cmd_list[1] == '**$BAR**', cmd_list[1]
+
+        cmd_list = scons_subst_list(input_list,
+                                    DummyEnv({ 'FOO' : 'BAZ',
+                                               'BAR' : 'BLAT' }),
+                                    mode=SUBST_SIG)
+        map(lambda x, e=escape_func: x.escape(e), cmd_list[0])
+        cmd_list = map(str, cmd_list[0])
+        assert cmd_list[0] == 'BAZ', cmd_list[0]
+        assert cmd_list[1] == '**BLEH**', cmd_list[1]
+
     def test_mapPaths(self):
         """Test the mapPaths function"""
-        fs = SCons.Node.FS.FS()
-        dir=fs.Dir('foo')
-        file=fs.File('bar/file')
+        class MyFileNode:
+            def __init__(self, path):
+                self.path = path
+            def __str__(self):
+                return self.path
+            
+        dir=MyFileNode('foo')
+        file=MyFileNode('bar/file')
         
         class DummyEnv:
             def subst(self, arg):
@@ -618,11 +657,11 @@ class UtilTestCase(unittest.TestCase):
     def test_display(self):
         old_stdout = sys.stdout
         sys.stdout = OutBuffer()
-        SCons.Util.display("line1")
+        display("line1")
         display.set_mode(0)
-        SCons.Util.display("line2")
+        display("line2")
         display.set_mode(1)
-        SCons.Util.display("line3")
+        display("line3")
 
         assert sys.stdout.buffer == "line1\nline3\n"
         sys.stdout = old_stdout
@@ -648,12 +687,12 @@ class UtilTestCase(unittest.TestCase):
               "Removed " + os.path.join(base, xxx) + '\n' + \
               "Removed directory " + base + '\n'
 
-        SCons.Util.fs_delete(base, remove=0)
+        fs_delete(base, remove=0)
         assert sys.stdout.buffer == exp, sys.stdout.buffer
         assert os.path.exists(sub1_yyy)
 
         sys.stdout.buffer = ""
-        SCons.Util.fs_delete(base, remove=1)
+        fs_delete(base, remove=1)
         assert sys.stdout.buffer == exp
         assert not os.path.exists(base)
 
@@ -666,7 +705,7 @@ class UtilTestCase(unittest.TestCase):
         filename = tempfile.mktemp()
         str = '1234567890 ' + filename
         open(filename, 'w').write(str)
-        assert open(SCons.Util.get_native_path(filename)).read() == str
+        assert open(get_native_path(filename)).read() == str
 
     def test_subst_dict(self):
         """Test substituting dictionary values in an Action
@@ -675,14 +714,24 @@ class UtilTestCase(unittest.TestCase):
         assert d['a'] == 'A', d
         assert d['b'] == 'B', d
 
-        d = subst_dict(target = 't', source = 's', env=DummyEnv())
-        assert str(d['TARGETS']) == 't', d['TARGETS']
+        class SimpleNode:
+            def __init__(self, data):
+                self.data = data
+            def __str__(self):
+                return self.data
+            def rfile(self):
+                return self
+            def is_literal(self):
+                return 1
+            
+        d = subst_dict(target = SimpleNode('t'), source = SimpleNode('s'), env=DummyEnv())
+        assert str(d['TARGETS'][0]) == 't', d['TARGETS']
         assert str(d['TARGET']) == 't', d['TARGET']
-        assert str(d['SOURCES']) == 's', d['SOURCES']
+        assert str(d['SOURCES'][0]) == 's', d['SOURCES']
         assert str(d['SOURCE']) == 's', d['SOURCE']
 
-        d = subst_dict(target = ['t1', 't2'],
-                       source = ['s1', 's2'],
+        d = subst_dict(target = [SimpleNode('t1'), SimpleNode('t2')],
+                       source = [SimpleNode('s1'), SimpleNode('s2')],
                        env = DummyEnv())
         TARGETS = map(lambda x: str(x), d['TARGETS'])
         TARGETS.sort()
@@ -698,11 +747,11 @@ class UtilTestCase(unittest.TestCase):
                 self.name = name
             def __str__(self):
                 return self.name
-            def rstr(self):
-                return 'rstr-' + self.name
+            def rfile(self):
+                return self.__class__('rstr-' + self.name)
 
-        d = subst_dict(target = [N('t3'), 't4'],
-                       source = ['s3', N('s4')],
+        d = subst_dict(target = [N('t3'), SimpleNode('t4')],
+                       source = [SimpleNode('s3'), N('s4')],
                        env = DummyEnv())
         TARGETS = map(lambda x: str(x), d['TARGETS'])
         TARGETS.sort()
@@ -711,6 +760,29 @@ class UtilTestCase(unittest.TestCase):
         SOURCES.sort()
         assert SOURCES == ['rstr-s4', 's3'], d['SOURCES']
 
+    def test_NodeList(self):
+        """Test NodeList class"""
+        class TestClass:
+            def __init__(self, name, child=None):
+                self.child = child
+                self.bar = name
+            def foo(self):
+                return self.bar + "foo"
+            def getself(self):
+                return self
+
+        t1 = TestClass('t1', TestClass('t1child'))
+        t2 = TestClass('t2', TestClass('t2child'))
+        t3 = TestClass('t3')
+
+        nl = NodeList([t1, t2, t3])
+        assert nl.foo() == [ 't1foo', 't2foo', 't3foo' ], nl.foo()
+        assert nl.bar == [ 't1', 't2', 't3' ], nl.bar
+        assert nl.getself().bar == [ 't1', 't2', 't3' ], nl.getself().bar
+        assert nl[0:2].child.foo() == [ 't1childfoo', 't2childfoo' ], \
+               nl[0:2].child.foo()
+        assert nl[0:2].child.bar == [ 't1child', 't2child' ], \
+               nl[0:2].child.bar
 
 if __name__ == "__main__":
     suite = unittest.makeSuite(UtilTestCase, 'test_')
diff --git a/test/CacheDir.py b/test/CacheDir.py
index 24c7e60..de78e7a 100644
--- a/test/CacheDir.py
+++ b/test/CacheDir.py
@@ -239,7 +239,7 @@ CacheDir(r'%s')
 
 def docopy(target,source,env):
     data = source[0].get_contents()
-    f = open(target[0].rfile().abspath, "wb")
+    f = open(target[0].rfile().get_abspath(), "wb")
     f.write(data)
     f.close()
 
diff --git a/test/scan-once.py b/test/scan-once.py
index ba147cf..f629db6 100644
--- a/test/scan-once.py
+++ b/test/scan-once.py
@@ -332,7 +332,7 @@ Mylib.ExportLib(env, lib_fullname)
 #cmd_justlib = "cd %s ; make" % Dir(".")
 
 cmd_generated = "%s $SOURCE" % (sys.executable,)
-cmd_justlib = "%s %s -C ${SOURCE[0].dir}" % (sys.executable, sys.argv[0])
+cmd_justlib = "%s %s -C ${SOURCES[0].dir}" % (sys.executable, sys.argv[0])
 
 ##### Deps appear correct ... but wacky scanning?
 # Why?
-- 
cgit v0.12