Commits

Anonymous committed 3a37cbd

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

  • Participants
  • Parent commits bb77e3c

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()