Commits

Anonymous committed 3a37cbd

Add support for changing directory when executing Actions (the to the target directory by default).

Comments (0)

Files changed (12)

 to get at the Node that actually represents
 the object file.
 
+Builder calls support a
+.B chdir
+keyword argument that
+specifies that the Builder's action(s)
+should be executed
+after changing directory.
+If the
+.B chdir
+argument is
+a string or a directory Node,
+scons will change to the specified directory.
+If the
+.B chdir
+is not a string or Node
+and is non-zero,
+then scons will change to the
+target file's directory.
+
+.ES
+# scons will change to the "sub" subdirectory
+# before executing the "cp" command.
+env.Command('sub/dir/foo.out', 'sub/dir/foo.in',
+            "cp dir/foo.in dir/foo.out",
+            chdir='sub')
+
+# Because chdir is not a string, scons will change to the
+# target's directory ("sub/dir") before executing the
+# "cp" command.
+env.Command('sub/dir/foo.out', 'sub/dir/foo.in',
+            "cp foo.in foo.out",
+            chdir=1)
+.EE
+
+Note that scons will
+.I not
+automatically modify
+its expansion of
+construction variables like
+.B $TARGET
+and
+.B $SOURCE
+when using the chdir
+keyword argument--that is,
+the expanded file names
+will still be relative to
+the top-level SConstruct directory,
+and consequently incorrect
+relative to the chdir directory.
+If you use the chdir keyword argument,
+you will typically need to supply a different
+command line using
+expansions like
+.B ${TARGET.file}
+and
+.B ${SOURCE.file}
+to use just the filename portion of the
+targets and source.
+
 .B scons
 provides the following builder methods:
 
 env.MyBuild('foo.out', 'foo.in', my_arg = 'xyzzy')
 .EE
 
+.IP chdir
+A directory from which scons
+will execute the
+action(s) specified
+for this Builder.
+If the
+.B chdir
+argument is
+a string or a directory Node,
+scons will change to the specified directory.
+If the
+.B chdir
+is not a string or Node
+and is non-zero,
+then scons will change to the
+target file's directory.
+
+Note that scons will
+.I not
+automatically modify
+its expansion of
+construction variables like
+.B $TARGET
+and
+.B $SOURCE
+when using the chdir
+keyword argument--that is,
+the expanded file names
+will still be relative to
+the top-level SConstruct directory,
+and consequently incorrect
+relative to the chdir directory.
+Builders created using chdir keyword argument,
+will need to use construction variable
+expansions like
+.B ${TARGET.file}
+and
+.B ${SOURCE.file}
+to use just the filename portion of the
+targets and source.
+
+.ES
+b = Builder(action="build < ${SOURCE.file} > ${TARGET.file}",
+            chdir=1)
+env = Environment(BUILDERS = {'MyBuild' : b})
+env.MyBuild('sub/dir/foo.out', 'sub/dir/foo.in')
+.EE
+
 .RE
 Any additional keyword arguments supplied
 when a Builder object is created
 a = Action(build_it)
 .EE
 
+If the action argument is not one of the above,
+None is returned.
+
 The second, optional argument
 is a Python function that returns
 a string to be printed to describe the action being executed.
 a = Action(build_it, varlist=['XXX'])
 .EE
 .PP
-If the action argument is not one of the above,
-None is returned.
+
+The
+.BR Action ()
+global function
+also takes a
+.B chdir
+keyword argument
+which specifies that
+scons will execute the action
+after changing to the specified directory.
+If the chdir argument is
+a string or a directory Node,
+scons will change to the specified directory.
+If the chdir argument
+is not a string or Node
+and is non-zero,
+then scons will change to the
+target file's directory.
+
+Note that scons will
+.I not
+automatically modify
+its expansion of
+construction variables like
+.B $TARGET
+and
+.B $SOURCE
+when using the chdir
+keyword argument--that is,
+the expanded file names
+will still be relative to
+the top-level SConstruct directory,
+and consequently incorrect
+relative to the chdir directory.
+Builders created using chdir keyword argument,
+will need to use construction variable
+expansions like
+.B ${TARGET.file}
+and
+.B ${SOURCE.file}
+to use just the filename portion of the
+targets and source.
+
+.ES
+a = Action("build < ${SOURCE.file} > ${TARGET.file}",
+           chdir=1)
+.EE
 
 .SS Miscellaneous Action Functions
 
   - Add a ParseDepends() function that will parse up a list of explicit
     dependencies from a "make depend" style file.
 
+  - Support the ability to change directory when executing an Action
+    through "chdir" keyword arguments to Action and Builder creation
+    and calls.
+
   From Clive Levinson:
 
   - Make ParseConfig() recognize and add -mno-cygwin to $LINKFLAGS and

src/engine/SCons/Action.py

             return apply(ListAction, (listCmdActions,)+args, kw)
     return None
 
-def Action(act, strfunction=_null, varlist=[], presub=_null):
+def Action(act, *args, **kw):
     """A factory for action objects."""
     if SCons.Util.is_List(act):
-        acts = map(lambda x, s=strfunction, v=varlist, ps=presub:
-                          _do_create_action(x, strfunction=s, varlist=v, presub=ps),
+        acts = map(lambda a, args=args, kw=kw:
+                          apply(_do_create_action, (a,)+args, kw),
                    act)
-        acts = filter(lambda x: not x is None, acts)
+        acts = filter(None, acts)
         if len(acts) == 1:
             return acts[0]
         else:
-            return ListAction(acts, strfunction=strfunction, varlist=varlist, presub=presub)
+            return apply(ListAction, (acts,)+args, kw)
     else:
-        return _do_create_action(act, strfunction=strfunction, varlist=varlist, presub=presub)
+        return apply(_do_create_action, (act,)+args, kw)
 
 class ActionBase:
     """Base class for actions that create output objects."""
-    def __init__(self, strfunction=_null, presub=_null, **kw):
+    def __init__(self, strfunction=_null, presub=_null, chdir=None, **kw):
         if not strfunction is _null:
             self.strfunction = strfunction
         if presub is _null:
-            self.presub = print_actions_presub
-        else:
-            self.presub = presub
+            presub = print_actions_presub
+        self.presub = presub
+        self.chdir = chdir
 
     def __cmp__(self, other):
         return cmp(self.__dict__, other)
                                errfunc=None,
                                presub=_null,
                                show=_null,
-                               execute=_null):
+                               execute=_null,
+                               chdir=_null):
         if not SCons.Util.is_List(target):
             target = [target]
         if not SCons.Util.is_List(source):
         if presub is _null:  presub = self.presub
         if show is _null:  show = print_actions
         if execute is _null:  execute = execute_actions
+        if chdir is _null: chdir = self.chdir
+        save_cwd = None
+        if chdir:
+            save_cwd = os.getcwd()
+            try:
+                chdir = str(chdir.abspath)
+            except AttributeError:
+                if not SCons.Util.is_String(chdir):
+                    chdir = str(target[0].dir)
         if presub:
             t = string.join(map(str, target), 'and')
             l = string.join(self.presub_lines(env), '\n  ')
             out = "Building %s with action(s):\n  %s\n" % (t, l)
             sys.stdout.write(out)
+        s = None
         if show and self.strfunction:
             s = self.strfunction(target, source, env)
             if s:
+                if chdir:
+                    s = ('os.chdir(%s)\n' % repr(chdir)) + s
                 try:
                     get = env.get
                 except AttributeError:
                     if not print_func:
                         print_func = self.print_cmd_line
                 print_func(s, target, source, env)
+        stat = 0
         if execute:
-            stat = self.execute(target, source, env)
-            if stat and errfunc:
-                errfunc(stat)
-            return stat
-        else:
-            return 0
+            if chdir:
+                os.chdir(chdir)
+            try:
+                stat = self.execute(target, source, env)
+                if stat and errfunc:
+                    errfunc(stat)
+            finally:
+                if save_cwd:
+                    os.chdir(save_cwd)
+        if s and save_cwd:
+            print_func('os.chdir(%s)' % repr(save_cwd), target, source, env)
+        return stat
 
     def presub_lines(self, env):
         # CommandGeneratorAction needs a real environment
     def genstring(self, target, source, env):
         return str(self)
 
-    def get_actions(self):
-        return [self]
-
     def __add__(self, other):
         return _actionAppend(self, other)
 
 
 class CommandAction(ActionBase):
     """Class for command-execution actions."""
-    def __init__(self, cmd, **kw):
+    def __init__(self, cmd, *args, **kw):
         # Cmd list can actually be a list or a single item...basically
         # anything that we could pass in as the first arg to
         # Environment.subst_list().
         if __debug__: logInstanceCreation(self)
-        apply(ActionBase.__init__, (self,), kw)
+        apply(ActionBase.__init__, (self,)+args, kw)
         self.cmd_list = cmd
 
     def __str__(self):
 
 class CommandGeneratorAction(ActionBase):
     """Class for command-generator actions."""
-    def __init__(self, generator, **kw):
+    def __init__(self, generator, *args, **kw):
         if __debug__: logInstanceCreation(self)
-        apply(ActionBase.__init__, (self,), kw)
+        apply(ActionBase.__init__, (self,)+args, kw)
         self.generator = generator
 
     def __generate(self, target, source, env, for_signature):
 class FunctionAction(ActionBase):
     """Class for Python function actions."""
 
-    def __init__(self, execfunction, **kw):
+    def __init__(self, execfunction, *args, **kw):
         if __debug__: logInstanceCreation(self)
         self.execfunction = execfunction
-        apply(ActionBase.__init__, (self,), kw)
+        apply(ActionBase.__init__, (self,)+args, kw)
         self.varlist = kw.get('varlist', [])
 
     def function_name(self):
 
 class ListAction(ActionBase):
     """Class for lists of other actions."""
-    def __init__(self, list, **kw):
+    def __init__(self, list, *args, **kw):
         if __debug__: logInstanceCreation(self)
-        apply(ActionBase.__init__, (self,), kw)
+        apply(ActionBase.__init__, (self,)+args, kw)
         self.list = map(lambda x: Action(x), list)
 
-    def get_actions(self):
-        return self.list
-
     def __str__(self):
         s = []
         for l in self.list:

src/engine/SCons/ActionTests.py

         """Test creation of ActionBase objects
         """
 
-        def func():
+        def func1():
+            pass
+
+        def func2():
             pass
 
         a = SCons.Action.ActionBase()
         assert not hasattr(a, 'strfunction')
         assert not hasattr(a, 'kwarg')
 
-        a = SCons.Action.ActionBase(func)
-        assert a.strfunction is func, a.strfunction
+        a = SCons.Action.ActionBase(strfunction=func1)
+        assert a.strfunction is func1, a.strfunction
 
-        a = SCons.Action.ActionBase(strfunction=func)
-        assert a.strfunction is func, a.strfunction
+        a = SCons.Action.ActionBase(presub=func1)
+        assert a.presub is func1, a.presub
+
+        a = SCons.Action.ActionBase(chdir=1)
+        assert a.chdir is 1, a.chdir
+
+        a = SCons.Action.ActionBase(func1, func2, 'x')
+        assert a.strfunction is func1, a.strfunction
+        assert a.presub is func2, a.presub
+        assert a.chdir is 'x', a.chdir
 
     def test___cmp__(self):
         """Test Action comparison
         save_execute_actions = SCons.Action.execute_actions
         #SCons.Action.print_actions = 0
 
+        test = TestCmd.TestCmd(workdir = '')
+        test.subdir('sub', 'xyz')
+        os.chdir(test.workpath())
+
         try:
             env = Environment()
 
             s = sio.getvalue()
             assert s == 'execfunc(["out"], ["in"])\n', s
 
+            a.chdir = 'xyz'
+            expect = 'os.chdir(\'%s\')\nexecfunc(["out"], ["in"])\nos.chdir(\'%s\')\n'
+
+            sio = StringIO.StringIO()
+            sys.stdout = sio
+            result = a("out", "in", env)
+            assert result == 7, result
+            s = sio.getvalue()
+            assert s == expect % ('xyz', test.workpath()), s
+
+            sio = StringIO.StringIO()
+            sys.stdout = sio
+            result = a("out", "in", env, chdir='sub')
+            assert result == 7, result
+            s = sio.getvalue()
+            assert s == expect % ('sub', test.workpath()), s
+
+            a.chdir = None
+
             SCons.Action.execute_actions = 0
 
             sio = StringIO.StringIO()
         s = a.presub_lines(Environment(ACT = 'expanded action'))
         assert s == ['expanded action'], s
 
-    def test_get_actions(self):
-        """Test the get_actions() method
-        """
-        a = SCons.Action.Action("x")
-        l = a.get_actions()
-        assert l == [a], l
-
     def test_add(self):
         """Test adding Actions to stuff."""
         # Adding actions to other Actions or to stuff that can
         assert isinstance(a.list[2], SCons.Action.ListAction)
         assert a.list[2].list[0].cmd_list == 'y'
 
-    def test_get_actions(self):
-        """Test the get_actions() method for ListActions
-        """
-        a = SCons.Action.ListAction(["x", "y"])
-        l = a.get_actions()
-        assert len(l) == 2, l
-        assert isinstance(l[0], SCons.Action.CommandAction), l[0]
-        g = l[0].get_actions()
-        assert g == [l[0]], g
-        assert isinstance(l[1], SCons.Action.CommandAction), l[1]
-        g = l[1].get_actions()
-        assert g == [l[1]], g
-
     def test___str__(self):
         """Test the __str__() method for a list of subsidiary Actions
         """

src/engine/SCons/Builder.py

 
     return ret
 
-def _init_nodes(builder, env, overrides, tlist, slist):
+def _init_nodes(builder, env, overrides, executor_kw, tlist, slist):
     """Initialize lists of target and source nodes with all of
     the proper Builder information.
     """
                                            env or builder.env,
                                            [builder.overrides, overrides],
                                            tlist,
-                                           slist)
+                                           slist,
+                                           executor_kw)
 
     # Now set up the relevant information in the target Nodes themselves.
     for t in tlist:
                         env = None,
                         single_source = 0,
                         name = None,
+                        chdir = _null,
                         **overrides):
         if __debug__: logInstanceCreation(self, 'BuilderBase')
         self.action = SCons.Action.Action(action)
         # that don't get attached to construction environments.
         if name:
             self.name = name
+        self.executor_kw = {}
+        if not chdir is _null:
+            self.executor_kw['chdir'] = chdir
 
     def __nonzero__(self):
         raise InternalError, "Do not test for the Node.builder attribute directly; use Node.has_builder() instead"
 
         return tlist, slist
 
-    def _execute(self, env, target = None, source = _null, overwarn={}):
+    def _execute(self, env, target=None, source=_null, overwarn={}, executor_kw={}):
         if source is _null:
             source = target
             target = None
             builder = self
         else:
             builder = ListBuilder(self, env, tlist)
-        _init_nodes(builder, env, overwarn.data, tlist, slist)
+        _init_nodes(builder, env, overwarn.data, executor_kw, tlist, slist)
 
         return tlist
 
-    def __call__(self, env, target = None, source = _null, **kw):
-        return self._execute(env, target, source, OverrideWarner(kw))
+    def __call__(self, env, target=None, source=_null, chdir=_null, **kw):
+        if chdir is _null:
+            ekw = self.executor_kw
+        else:
+            ekw = self.executor_kw.copy()
+            ekw['chdir'] = chdir
+        return self._execute(env, target, source, OverrideWarner(kw), ekw)
 
     def adjust_suffix(self, suff):
         if suff and not suff[0] in [ '.', '_', '$' ]:
         self.sdict = {}
         self.cached_src_suffixes = {} # source suffixes keyed on id(env)
 
-    def _execute(self, env, target = None, source = _null, overwarn={}):
+    def _execute(self, env, target = None, source = _null, overwarn={}, executor_kw={}):
         if source is _null:
             source = target
             target = None

src/engine/SCons/Executor.py

     and sources for later processing as needed.
     """
 
-    def __init__(self, action, env=None, overridelist=[], targets=[], sources=[]):
+    def __init__(self, action, env=None, overridelist=[],
+                 targets=[], sources=[], builder_kw={}):
         if __debug__: logInstanceCreation(self)
+        if not action:
+            raise SCons.Errors.UserError, "Executor must have an action."
         self.action = action
         self.env = env
         self.overridelist = overridelist
         self.targets = targets
         self.sources = sources[:]
-        if not action:
-            raise SCons.Errors.UserError, "Executor must have an action."
+        self.builder_kw = builder_kw
 
     def get_build_env(self):
         """Fetch or create the appropriate build Environment
         involved, so only one target's pre- and post-actions will win,
         anyway.  This is probably a bug we should fix...
         """
-        try:
-            al = self.action_list
-        except AttributeError:
-            al = self.action.get_actions()
-            self.action_list = al
+        al = [self.action]
         try:
             # XXX shouldn't reach into node attributes like this
             return target.pre_actions + al + target.post_actions
         if not action_list:
             return
         env = self.get_build_env()
+        kw = kw.copy()
+        kw.update(self.builder_kw)
         for action in action_list:
             apply(action, (self.targets, self.sources, env, errfunc), kw)
 

src/engine/SCons/ExecutorTests.py

 class MyAction:
     def __init__(self, actions=['action1', 'action2']):
         self.actions = actions
-    def get_actions(self):
-        return self.actions
+    def __call__(self, target, source, env, errfunc, **kw):
+        for action in self.actions:
+            action(target, source, env, errfunc)
     def strfunction(self, target, source, env):
         return string.join(['STRFUNCTION'] + self.actions + target + source)
     def genstring(self, target, source, env):
     def test_get_action_list(self):
         """Test fetching and generating an action list"""
         x = SCons.Executor.Executor('b', 'e', 'o', 't', 's')
-        x.action_list = ['aaa']
         al = x.get_action_list(MyNode([], []))
-        assert al == ['aaa'], al
+        assert al == ['b'], al
         al = x.get_action_list(MyNode(['PRE'], ['POST']))
-        assert al == ['PRE', 'aaa', 'POST'], al
+        assert al == ['PRE', 'b', 'POST'], al
 
-        x = SCons.Executor.Executor(MyAction(), None, {}, 't', 's')
+        a = MyAction()
+        x = SCons.Executor.Executor(a, None, {}, 't', 's')
         al = x.get_action_list(MyNode(['pre'], ['post']))
-        assert al == ['pre', 'action1', 'action2', 'post'], al
+        assert al == ['pre', a, 'post'], al
 
     def test__call__(self):
         """Test calling an Executor"""

src/engine/SCons/Node/FS.py

         kids.sort(c)
         self._add_child(self.implicit, self.implicit_dict, kids)
 
-    def get_actions(self):
-        """A null "builder" for directories."""
-        return []
-
     def build(self, **kw):
         """A null "builder" for directories."""
         global MkdirBuilder

src/engine/SCons/Node/FSTests.py

         pass
     def strfunction(self, targets, sources, env):
         return ""
-    def get_actions(self):
-        return [self]
-
 class Builder:
     def __init__(self, factory, action=Action()):
         self.factory = factory
         self.overrides = {}
         self.action = action
 
-    def get_actions(self):
-        return [self]
-
     def targets(self, t):
         return [t]
 
         dir = fs.Dir("dir")
         dir.prepare()
 
-class get_actionsTestCase(unittest.TestCase):
-    def runTest(self):
-        """Test the Dir's get_action() method"""
-
-        fs = SCons.Node.FS.FS()
-        dir = fs.Dir('.')
-        a = dir.get_actions()
-        assert a == [], a
-
 class SConstruct_dirTestCase(unittest.TestCase):
     def runTest(self):
         """Test setting the SConstruct directory"""
     suite.addTest(stored_infoTestCase())
     suite.addTest(has_src_builderTestCase())
     suite.addTest(prepareTestCase())
-    suite.addTest(get_actionsTestCase())
     suite.addTest(SConstruct_dirTestCase())
     suite.addTest(CacheDirTestCase())
     suite.addTest(clearTestCase())

test/SConscriptChdir.py

+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.subdir('dir1', 'dir2', 'dir3', 'dir4', 'dir5')
+
+test.write('SConstruct', """
+env = Environment()
+SConscript('dir1/SConscript')
+SConscriptChdir(1)
+SConscript('dir2/SConscript')
+SConscriptChdir(0)
+SConscript('dir3/SConscript')
+env.SConscriptChdir(1)
+SConscript('dir4/SConscript')
+env.SConscriptChdir(0)
+SConscript('dir5/SConscript')
+""")
+
+test.write(['dir1', 'SConscript'], """
+execfile("create_test.py")
+""")
+
+test.write(['dir2', 'SConscript'], """
+execfile("create_test.py")
+""")
+
+test.write(['dir3', 'SConscript'], """
+import os.path
+name = os.path.join('dir3', 'create_test.py')
+execfile(name)
+""")
+
+test.write(['dir4', 'SConscript'], """
+execfile("create_test.py")
+""")
+
+test.write(['dir5', 'SConscript'], """
+import os.path
+name = os.path.join('dir5', 'create_test.py')
+execfile(name)
+""")
+
+for dir in ['dir1', 'dir2', 'dir3','dir4', 'dir5']:
+    test.write([dir, 'create_test.py'], r"""
+f = open("test.txt", "ab")
+f.write("This is the %s test.\n")
+f.close()
+""" % dir)
+
+test.run(arguments=".", stderr=None)
+
+test.fail_test(test.read(['dir1', 'test.txt']) != "This is the dir1 test.\n")
+test.fail_test(test.read(['dir2', 'test.txt']) != "This is the dir2 test.\n")
+test.fail_test(test.read('test.txt') != "This is the dir3 test.\nThis is the dir5 test.\n")
+test.fail_test(test.read(['dir4', 'test.txt']) != "This is the dir4 test.\n")
+
+test.pass_test()
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Test that the chdir argument to Builder creation, Action creation,
+Command() calls and execution work1s correctly.
+"""
+
+import TestSCons
+
+python = TestSCons.python
+
+test = TestSCons.TestSCons()
+
+test.subdir('other1',
+            'other2',
+            'other3',
+            'other4',
+            'other5',
+            'other6',
+            'other7',
+            'other8',
+            'other9',
+            'work1',
+            ['work1', 'sub1'],
+            ['work1', 'sub2'],
+            ['work1', 'sub3'],
+            ['work1', 'sub4'],
+            ['work1', 'sub5'],
+            ['work1', 'sub6'],
+            ['work1', 'sub7'],
+            ['work1', 'sub8'],
+            ['work1', 'sub9'],
+            ['work1', 'sub20'],
+            ['work1', 'sub21'],
+            ['work1', 'sub22'],
+            ['work1', 'sub23'])
+
+cat_py = test.workpath('cat.py')
+
+other1 = test.workpath('other1')
+other1_f11_out = test.workpath('other1', 'f11.out')
+other1_f11_in = test.workpath('other1', 'f11.in')
+other2 = test.workpath('other2')
+other2_f12_out = test.workpath('other2', 'f12.out')
+other2_f12_in = test.workpath('other2', 'f12.in')
+other3 = test.workpath('other3')
+other3_f13_out = test.workpath('other3', 'f13.out')
+other3_f13_in = test.workpath('other3', 'f13.in')
+other4 = test.workpath('other4')
+other4_f14_out = test.workpath('other4', 'f14.out')
+other4_f14_in = test.workpath('other4', 'f14.in')
+other5 = test.workpath('other5')
+other5_f15_out = test.workpath('other5', 'f15.out')
+other5_f15_in = test.workpath('other5', 'f15.in')
+other6 = test.workpath('other6')
+other6_f16_out = test.workpath('other6', 'f16.out')
+other6_f16_in = test.workpath('other6', 'f16.in')
+other7 = test.workpath('other7')
+other7_f17_out = test.workpath('other7', 'f17.out')
+other7_f17_in = test.workpath('other7', 'f17.in')
+other8 = test.workpath('other8')
+other8_f18_out = test.workpath('other8', 'f18.out')
+other8_f18_in = test.workpath('other8', 'f18.in')
+other9 = test.workpath('other9')
+other9_f19_out = test.workpath('other9', 'f19.out')
+other9_f19_in = test.workpath('other9', 'f19.in')
+
+test.write(cat_py, """\
+import sys
+ofp = open(sys.argv[1], 'w')
+for ifp in map(open, sys.argv[2:]):
+    ofp.write(ifp.read())
+ofp.close
+""")
+
+test.write(['work1', 'SConstruct'], """
+cat_command = r"%(python)s %(cat_py)s ${TARGET.file} ${SOURCE.file}"
+
+no_chdir_act = Action(cat_command)
+chdir_sub4_act = Action(cat_command, chdir=1)
+chdir_sub5_act = Action(cat_command, chdir='sub5')
+chdir_sub6_act = Action(cat_command, chdir=Dir('sub6'))
+
+env = Environment(BUILDERS = {
+    'Chdir4' : Builder(action = chdir_sub4_act),
+    'Chdir5' : Builder(action = chdir_sub5_act),
+    'Chdir6' : Builder(action = chdir_sub6_act),
+    'Chdir7' : Builder(action = no_chdir_act, chdir=1),
+    'Chdir8' : Builder(action = no_chdir_act, chdir='sub8'),
+    'Chdir9' : Builder(action = no_chdir_act, chdir=Dir('sub9')),
+})
+
+env.Command('f0.out', 'f0.in', cat_command)
+
+env.Command('sub1/f1.out', 'sub1/f1.in', cat_command,
+            chdir=1)
+env.Command('sub2/f2.out', 'sub2/f2.in', cat_command,
+            chdir='sub2')
+env.Command('sub3/f3.out', 'sub3/f3.in', cat_command,
+            chdir=Dir('sub3'))
+
+env.Chdir4('sub4/f4.out', 'sub4/f4.in')
+env.Chdir5('sub5/f5.out', 'sub5/f5.in')
+env.Chdir6('sub6/f6.out', 'sub6/f6.in')
+
+env.Chdir7('sub7/f7.out', 'sub7/f7.in')
+env.Chdir8('sub8/f8.out', 'sub8/f8.in')
+env.Chdir9('sub9/f9.out', 'sub9/f9.in')
+
+env.Command(r'%(other1_f11_out)s', r'%(other1_f11_in)s', cat_command,
+            chdir=1)
+env.Command(r'%(other2_f12_out)s', r'%(other2_f12_in)s', cat_command,
+            chdir=r'%(other2)s')
+env.Command(r'%(other3_f13_out)s', r'%(other3_f13_in)s', cat_command,
+            chdir=Dir(r'%(other3)s'))
+
+env.Chdir4(r'%(other4_f14_out)s', r'%(other4_f14_in)s')
+env.Chdir5(r'%(other5_f15_out)s', r'%(other5_f15_in)s',
+           chdir=r'%(other5)s')
+env.Chdir6(r'%(other6_f16_out)s', r'%(other6_f16_in)s',
+           chdir=Dir(r'%(other6)s'))
+
+env.Chdir7(r'%(other7_f17_out)s', r'%(other7_f17_in)s')
+env.Chdir8(r'%(other8_f18_out)s', r'%(other8_f18_in)s',
+           chdir=r'%(other8)s')
+env.Chdir9(r'%(other9_f19_out)s', r'%(other9_f19_in)s',
+           chdir=Dir(r'%(other9)s'))
+
+Command('f20.out', 'f20.in', cat_command)
+
+Command('sub21/f21.out', 'sub21/f21.in', cat_command,
+        chdir=1)
+Command('sub22/f22.out', 'sub22/f22.in', cat_command,
+        chdir='sub22')
+Command('sub23/f23.out', 'sub23/f23.in', cat_command,
+        chdir=Dir('sub23'))
+""" % locals())
+
+test.write(['work1', 'f0.in'], "work1/f0.in\n")
+
+test.write(['work1', 'sub1', 'f1.in'], "work1/sub1/f1.in\n")
+test.write(['work1', 'sub2', 'f2.in'], "work1/sub2/f2.in\n")
+test.write(['work1', 'sub3', 'f3.in'], "work1/sub3/f3.in\n")
+test.write(['work1', 'sub4', 'f4.in'], "work1/sub4/f4.in\n")
+test.write(['work1', 'sub5', 'f5.in'], "work1/sub5/f5.in\n")
+test.write(['work1', 'sub6', 'f6.in'], "work1/sub6/f6.in\n")
+test.write(['work1', 'sub7', 'f7.in'], "work1/sub7/f7.in\n")
+test.write(['work1', 'sub8', 'f8.in'], "work1/sub8/f8.in\n")
+test.write(['work1', 'sub9', 'f9.in'], "work1/sub9/f9.in\n")
+
+test.write(['other1', 'f11.in'], "other1/f11.in\n")
+test.write(['other2', 'f12.in'], "other2/f12.in\n")
+test.write(['other3', 'f13.in'], "other3/f13.in\n")
+test.write(['other4', 'f14.in'], "other4/f14.in\n")
+test.write(['other5', 'f15.in'], "other5/f15.in\n")
+test.write(['other6', 'f16.in'], "other6/f16.in\n")
+test.write(['other7', 'f17.in'], "other7/f17.in\n")
+test.write(['other8', 'f18.in'], "other8/f18.in\n")
+test.write(['other9', 'f19.in'], "other9/f19.in\n")
+
+test.write(['work1', 'f20.in'], "work1/f20.in\n")
+
+test.write(['work1', 'sub21', 'f21.in'], "work1/sub21/f21.in\n")
+test.write(['work1', 'sub22', 'f22.in'], "work1/sub22/f22.in\n")
+test.write(['work1', 'sub23', 'f23.in'], "work1/sub23/f23.in\n")
+
+test.run(chdir='work1', arguments='..')
+
+test.must_match(['work1', 'f0.out'], "work1/f0.in\n")
+
+test.must_match(['work1', 'sub1', 'f1.out'], "work1/sub1/f1.in\n")
+test.must_match(['work1', 'sub2', 'f2.out'], "work1/sub2/f2.in\n")
+test.must_match(['work1', 'sub3', 'f3.out'], "work1/sub3/f3.in\n")
+test.must_match(['work1', 'sub4', 'f4.out'], "work1/sub4/f4.in\n")
+test.must_match(['work1', 'sub5', 'f5.out'], "work1/sub5/f5.in\n")
+test.must_match(['work1', 'sub6', 'f6.out'], "work1/sub6/f6.in\n")
+test.must_match(['work1', 'sub7', 'f7.out'], "work1/sub7/f7.in\n")
+test.must_match(['work1', 'sub8', 'f8.out'], "work1/sub8/f8.in\n")
+test.must_match(['work1', 'sub9', 'f9.out'], "work1/sub9/f9.in\n")
+
+test.must_match(['other1', 'f11.out'], "other1/f11.in\n")
+test.must_match(['other2', 'f12.out'], "other2/f12.in\n")
+test.must_match(['other3', 'f13.out'], "other3/f13.in\n")
+test.must_match(['other4', 'f14.out'], "other4/f14.in\n")
+test.must_match(['other5', 'f15.out'], "other5/f15.in\n")
+test.must_match(['other6', 'f16.out'], "other6/f16.in\n")
+test.must_match(['other7', 'f17.out'], "other7/f17.in\n")
+test.must_match(['other8', 'f18.out'], "other8/f18.in\n")
+test.must_match(['other9', 'f19.out'], "other9/f19.in\n")
+
+test.must_match(['work1', 'f20.out'], "work1/f20.in\n")
+
+test.must_match(['work1', 'sub21', 'f21.out'], "work1/sub21/f21.in\n")
+test.must_match(['work1', 'sub22', 'f22.out'], "work1/sub22/f22.in\n")
+test.must_match(['work1', 'sub23', 'f23.out'], "work1/sub23/f23.in\n")
+
+
+
+test.subdir('work2',
+            ['work2', 'sub'])
+
+work2 = test.workpath('work2')
+work2_sub_f1_out = test.workpath('work2', 'sub', 'f1.out')
+work2_sub_f2_out = test.workpath('work2', 'sub', 'f2.out')
+
+test.write(['work2', 'SConstruct'], """\
+cat_command = r"%(python)s %(cat_py)s ${TARGET.file} ${SOURCE.file}"
+env = Environment()
+env.Command('sub/f1.out', 'sub/f1.in', cat_command,
+            chdir=1)
+env.Command('sub/f2.out', 'sub/f2.in',
+            [
+              r"%(python)s %(cat_py)s .temp ${SOURCE.file}",
+              r"%(python)s %(cat_py)s ${TARGET.file} .temp",
+            ],
+            chdir=1)
+""" % locals())
+
+test.write(['work2', 'sub', 'f1.in'], "work2/sub/f1.in")
+test.write(['work2', 'sub', 'f2.in'], "work2/sub/f2.in")
+
+expect = test.wrap_stdout("""\
+os.chdir('sub')
+%(python)s %(cat_py)s f1.out f1.in
+os.chdir('%(work2)s')
+os.chdir('sub')
+%(python)s %(cat_py)s .temp f2.in
+%(python)s %(cat_py)s f2.out .temp
+os.chdir('%(work2)s')
+""" % locals())
+
+test.run(chdir='work2', arguments='-n .', stdout=expect)
+
+test.must_not_exist(work2_sub_f1_out)
+test.must_not_exist(work2_sub_f2_out)
+
+test.run(chdir='work2', arguments='.', stdout=expect)
+
+test.must_match(work2_sub_f1_out, "work2/sub/f1.in")
+test.must_match(work2_sub_f2_out, "work2/sub/f2.in")
+
+test.run(chdir='work2', arguments='-c .')
+
+test.must_not_exist(work2_sub_f1_out)
+test.must_not_exist(work2_sub_f2_out)
+
+test.run(chdir='work2', arguments='-s .', stdout="")
+
+test.must_match(work2_sub_f1_out, "work2/sub/f1.in")
+test.must_match(work2_sub_f2_out, "work2/sub/f2.in")
+
+test.pass_test()

test/strfunction.py

 %s cat.py list.in .temp
 %s cat.py .temp list.out
 Building liststr.out from liststr.in
-Building liststr.out from liststr.in
 """) % (python, python, python, python, python, python, python, python))
-# XXX The duplication of "Buiding liststr.out" above is WRONG!
-# A follow-on fix should take care of this.
 
 test.pass_test()