More flexible (and Make-like) ignoring command exit status, and suppressing printing...
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Tue, 26 Apr 2005 04:14:40 +0000 (04:14 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Tue, 26 Apr 2005 04:14:40 +0000 (04:14 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@1284 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/engine/SCons/Action.py
src/engine/SCons/ActionTests.py
src/engine/SCons/Executor.py
src/engine/SCons/Node/__init__.py
test/ignore-command.py [new file with mode: 0644]
test/silent-command.py [new file with mode: 0644]

index cb441ce1bbacc5a93bfe530f26b3a3db79212888..cb1334be0bbce1b969f9fead8d79bdc44c958118 100644 (file)
@@ -2765,10 +2765,20 @@ already specified in other Builder of function calls.)
 Any other keyword arguments specified override any
 same-named existing construction variables.
 
-Note that an action can be an external command,
+An action can be an external command,
 specified as a string,
 or a callable Python object;
-see "Action Objects," below.
+see "Action Objects," below,
+for more complete information.
+Also note that a string specifying an external command
+may be preceded by an
+.B @
+(at-sign)
+to suppress printing the command in question,
+or by a
+.B \-
+(hyphen)
+to ignore the exit status of the external command.
 Examples:
 
 .ES
@@ -8613,9 +8623,27 @@ the object is simply returned.
 .IP String
 If the first argument is a string,
 a command-line Action is returned.
+Note that the command line string
+may be preceded by an
+.B @
+(at-sign)
+to suppress printing of the
+specified command line,
+or by a
+.B \-
+(hyphen)
+to ignore the exit status from
+the specified command.
+Examples:
 
 .ES
 Action('$CC -c -o $TARGET $SOURCES')
+
+# Doesn't print the line being executed.
+Action('@build $TARGET $SOURCES')
+
+# Ignores
+Action('-build $TARGET $SOURCES')
 .EE
 
 .\" XXX From Gary Ruben, 23 April 2002:
@@ -8805,6 +8833,32 @@ a = Action("build < ${SOURCE.file} > ${TARGET.file}",
            chdir=1)
 .EE
 
+The
+.BR Action ()
+global function
+also takes an
+.B exitstatfunc
+keyword argument
+which specifies a function
+that is passed the exit status
+(or return value)
+from the specified action
+and can return an arbitrary
+or modified value.
+This can be used, for example,
+to specify that an Action object's
+return value should be ignored
+and SCons should, therefore,
+consider that the action always suceeds:
+
+.ES
+def always_succeed(s):
+    # Always return 0, which indicates success.
+    return 0
+a = Action("build < ${SOURCE.file} > ${TARGET.file}",
+           exitstatfunc=always_succeed)
+.EE
+
 .SS Miscellaneous Action Functions
 
 .B scons
index 3b0230c7dc5d07e6076959a06b035b8c1ac5886e..353ead35cf8fe32638a91f776da3ba9118af7f4c 100644 (file)
@@ -124,6 +124,9 @@ def rfile(n):
     except AttributeError:
         return n
 
+def default_exitstatfunc(s):
+    return s
+
 def _actionAppend(act1, act2):
     # This function knows how to slap two actions together.
     # Mainly, it handles ListActions by concatenating into
@@ -245,19 +248,22 @@ if not SCons.Memoize.has_metaclass:
 
 class _ActionAction(ActionBase):
     """Base class for actions that create output objects."""
-    def __init__(self, strfunction=_null, presub=_null, chdir=None, **kw):
+    def __init__(self, strfunction=_null, presub=_null, chdir=None, exitstatfunc=None, **kw):
         if not strfunction is _null:
             self.strfunction = strfunction
         if presub is _null:
             presub = print_actions_presub
         self.presub = presub
         self.chdir = chdir
+        if not exitstatfunc:
+            exitstatfunc = default_exitstatfunc
+        self.exitstatfunc = exitstatfunc
 
     def print_cmd_line(self, s, target, source, env):
         sys.stdout.write(s + "\n")
 
     def __call__(self, target, source, env,
-                               errfunc=None,
+                               exitstatfunc=_null,
                                presub=_null,
                                show=_null,
                                execute=_null,
@@ -266,6 +272,7 @@ class _ActionAction(ActionBase):
             target = [target]
         if not SCons.Util.is_List(source):
             source = [source]
+        if exitstatfunc is _null: exitstatfunc = self.exitstatfunc
         if presub is _null:  presub = self.presub
         if show is _null:  show = print_actions
         if execute is _null:  execute = execute_actions
@@ -304,8 +311,7 @@ class _ActionAction(ActionBase):
                 os.chdir(chdir)
             try:
                 stat = self.execute(target, source, env)
-                if stat and errfunc:
-                    errfunc(stat)
+                stat = exitstatfunc(stat)
             finally:
                 if save_cwd:
                     os.chdir(save_cwd)
@@ -350,12 +356,32 @@ class CommandAction(_ActionAction):
             return string.join(map(str, self.cmd_list), ' ')
         return str(self.cmd_list)
 
+    def process(self, target, source, env):
+        result = env.subst_list(self.cmd_list, 0, target, source)
+        silent = None
+        ignore = None
+        while 1:
+            try: c = result[0][0][0]
+            except IndexError: c = None
+            if c == '@': silent = 1
+            elif c == '-': ignore = 1
+            else: break
+            result[0][0] = result[0][0][1:]
+        try:
+            if not result[0][0]:
+                result[0] = result[0][1:]
+        except IndexError:
+            pass
+        return result, ignore, silent
+
     def strfunction(self, target, source, env):
         if not self.cmdstr is None:
             c = env.subst(self.cmdstr, 0, target, source)
             if c:
                 return c
-        cmd_list = env.subst_list(self.cmd_list, 0, target, source)
+        cmd_list, ignore, silent = self.process(target, source, env)
+       if silent:
+           return ''
         return _string_from_cmd_list(cmd_list[0])
 
     def execute(self, target, source, env):
@@ -406,15 +432,14 @@ class CommandAction(_ActionAction):
                     # reasonable for just about everything else:
                     ENV[key] = str(value)
 
-        cmd_list = env.subst_list(self.cmd_list, 0, target,
-                                  map(rfile, source))
+       cmd_list, ignore, silent = self.process(target, map(rfile, source), env)
 
         # Use len() to filter out any "command" that's zero-length.
         for cmd_line in filter(len, cmd_list):
             # Escape the command line for the interpreter we are using.
             cmd_line = escape_list(cmd_line, escape)
             result = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
-            if result:
+            if not ignore and result:
                 return result
         return 0
 
@@ -461,10 +486,10 @@ class CommandGeneratorAction(ActionBase):
     def genstring(self, target, source, env):
         return self._generate(target, source, env, 1).genstring(target, source, env)
 
-    def __call__(self, target, source, env, errfunc=None, presub=_null,
+    def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
                  show=_null, execute=_null, chdir=_null):
         act = self._generate(target, source, env, 0)
-        return act(target, source, env, errfunc, presub,
+        return act(target, source, env, exitstatfunc, presub,
                    show, execute, chdir)
 
     def get_contents(self, target, source, env):
@@ -650,10 +675,10 @@ class ListAction(ActionBase):
                                self.list),
                            "")
 
-    def __call__(self, target, source, env, errfunc=None, presub=_null,
+    def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
                  show=_null, execute=_null, chdir=_null):
         for act in self.list:
-            stat = act(target, source, env, errfunc, presub,
+            stat = act(target, source, env, exitstatfunc, presub,
                        show, execute, chdir)
             if stat:
                 return stat
index 3790c8cc16bd01d2492c5b45bf210a38fe5080c5..890abb2f1d51949da111ef07d1932cddf063aae0 100644 (file)
@@ -58,7 +58,8 @@ import TestCmd
 # for each test, they can just use the one.
 test = TestCmd.TestCmd(workdir = '')
 
-test.write('act.py', """import os, string, sys
+test.write('act.py', """\
+import os, string, sys
 f = open(sys.argv[1], 'w')
 f.write("act.py: '" + string.join(sys.argv[2:], "' '") + "'\\n")
 try:
@@ -81,7 +82,13 @@ if os.environ.has_key( 'ACTPY_PIPE' ):
 sys.exit(0)
 """)
 
+test.write('exit.py', """\
+import sys
+sys.exit(int(sys.argv[1]))
+""")
+
 act_py = test.workpath('act.py')
+exit_py = test.workpath('exit.py')
 
 outfile = test.workpath('outfile')
 outfile2 = test.workpath('outfile2')
@@ -319,7 +326,6 @@ class ActionBaseTestCase(unittest.TestCase):
     pass
  
 class _ActionActionTestCase(unittest.TestCase):
-    
 
     def test__init__(self):
         """Test creation of _ActionAction objects
@@ -527,18 +533,19 @@ class _ActionActionTestCase(unittest.TestCase):
             assert s == '', s
 
             sys.stdout = save_stdout
-            errfunc_result = []
+            exitstatfunc_result = []
 
-            def errfunc(stat, result=errfunc_result):
+            def exitstatfunc(stat, result=exitstatfunc_result):
                 result.append(stat)
+               return stat
 
-            result = a("out", "in", env, errfunc=errfunc)
+            result = a("out", "in", env, exitstatfunc=exitstatfunc)
             assert result == 0, result
-            assert errfunc_result == [], errfunc_result
+            assert exitstatfunc_result == [], exitstatfunc_result
 
-            result = a("out", "in", env, execute=1, errfunc=errfunc)
+            result = a("out", "in", env, execute=1, exitstatfunc=exitstatfunc)
             assert result == 7, result
-            assert errfunc_result == [7], errfunc_result
+            assert exitstatfunc_result == [7], exitstatfunc_result
 
             SCons.Action.execute_actions = 1
 
@@ -830,6 +837,38 @@ class CommandActionTestCase(unittest.TestCase):
         s = act4.strfunction([t1], [s1], env)
         assert s is None, s
 
+       act = SCons.Action.CommandAction("@foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "", s
+
+       act = SCons.Action.CommandAction("@-foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "", s
+
+       act = SCons.Action.CommandAction("-@foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "", s
+
+       act = SCons.Action.CommandAction("-foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "foo bar", s
+
+       act = SCons.Action.CommandAction("@ foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "", s
+
+       act = SCons.Action.CommandAction("@- foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "", s
+
+       act = SCons.Action.CommandAction("-@ foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "", s
+
+       act = SCons.Action.CommandAction("- foo bar")
+       s = act.strfunction([], [], env)
+       assert s == "foo bar", s
+
     def test_execute(self):
         """Test execution of command Actions
 
@@ -952,6 +991,34 @@ class CommandActionTestCase(unittest.TestCase):
         r = act([], [], env.Copy(out = outfile))
         assert r == expect_nonexecutable, "r == %d" % r
 
+       act = SCons.Action.CommandAction('%s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 1, r
+
+       act = SCons.Action.CommandAction('@%s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 1, r
+
+       act = SCons.Action.CommandAction('@-%s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 0, r
+
+       act = SCons.Action.CommandAction('-%s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 0, r
+
+       act = SCons.Action.CommandAction('@ %s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 1, r
+
+       act = SCons.Action.CommandAction('@- %s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 0, r
+
+       act = SCons.Action.CommandAction('- %s %s 1' % (python, exit_py))
+       r = act([], [], env)
+       assert r == 0, r
+
     def _DO_NOT_EXECUTE_test_pipe_execute(self):
         """Test capturing piped output from an action
 
index afe817a933845c658b5f9286a698363ea45e7b53..afd6c4995127e16844eef85651b4222ad0a5045c 100644 (file)
@@ -93,21 +93,21 @@ class Executor:
         result.update(kw)
         return result
 
-    def do_nothing(self, target, errfunc, kw):
+    def do_nothing(self, target, exitstatfunc, kw):
         pass
 
-    def do_execute(self, target, errfunc, kw):
+    def do_execute(self, target, exitstatfunc, kw):
         """Actually execute the action list."""
         apply(self.action,
-              (self.targets, self.sources, self.get_build_env(), errfunc),
+              (self.targets, self.sources, self.get_build_env(), exitstatfunc),
               self.get_kw(kw))
 
     # use extra indirection because with new-style objects (Python 2.2
     # and above) we can't override special methods, and nullify() needs
     # to be able to do this.
 
-    def __call__(self, target, errfunc, **kw):
-        self.do_execute(target, errfunc, kw)
+    def __call__(self, target, exitstatfunc, **kw):
+        self.do_execute(target, exitstatfunc, kw)
 
     def cleanup(self):
         "__reset_cache__"
index 28f1c194a74839262cfdce95d5e356e822ca9264..f9390eab0389a76d3ff73dd4a55cf64a93ce1616 100644 (file)
@@ -227,10 +227,12 @@ class Node:
         so only do thread safe stuff here. Do thread unsafe stuff in
         built().
         """
-        def errfunc(stat, node=self):
-            raise SCons.Errors.BuildError(node=node, errstr="Error %d" % stat)
+        def exitstatfunc(stat, node=self):
+            if stat:
+                msg = "Error %d" % stat
+                raise SCons.Errors.BuildError(node=node, errstr=msg)
         executor = self.get_executor()
-        apply(executor, (self, errfunc), kw)
+        apply(executor, (self, exitstatfunc), kw)
 
     def built(self):
         """Called just after this node is successfully built."""
diff --git a/test/ignore-command.py b/test/ignore-command.py
new file mode 100644 (file)
index 0000000..201c488
--- /dev/null
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001, 2002, 2003, 2004 The SCons Foundation
+#
+# 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.
+#
+
+"""
+Test use of a preceding - to ignore the return value from a command.
+"""
+
+__revision__ = "/home/scons/scons/branch.0/branch.96/baseline/test/option-n.py 0.96.C352 2005/03/26 00:09:23 knight"
+
+import os
+import os.path
+import re
+import string
+import sys
+import TestCmd
+import TestSCons
+
+python = TestSCons.python
+
+test = TestSCons.TestSCons()
+
+test.subdir('build', 'src')
+
+test.write('build.py', r"""
+import sys
+fp = open(sys.argv[1], 'wb')
+for f in sys.argv[2:]:
+    fp.write(open(f, 'rb').read())
+fp.close()
+sys.exit(1)
+""")
+
+test.write('SConstruct', """\
+env = Environment()
+f1 = env.Command('f1.out', 'f1.in', "%(python)s build.py $TARGET $SOURCE")
+f2 = env.Command('f2.out', 'f2.in', "-%(python)s build.py $TARGET $SOURCE")
+f3 = env.Command('f3.out', 'f3.in', "- %(python)s build.py $TARGET $SOURCE")
+f4 = env.Command('f4.out', 'f4.in', "@-%(python)s build.py $TARGET $SOURCE")
+f5 = env.Command('f5.out', 'f5.in', "@- %(python)s build.py $TARGET $SOURCE")
+f6 = env.Command('f6.out', 'f6.in', "-@%(python)s build.py $TARGET $SOURCE")
+f7 = env.Command('f7.out', 'f7.in', "-@ %(python)s build.py $TARGET $SOURCE")
+Default(f2, f3, f4, f5, f6, f7)
+""" % locals())
+
+test.write('f1.in', "f1.in\n")
+test.write('f2.in', "f2.in\n")
+test.write('f3.in', "f3.in\n")
+test.write('f4.in', "f4.in\n")
+test.write('f5.in', "f5.in\n")
+test.write('f6.in', "f6.in\n")
+test.write('f7.in', "f7.in\n")
+
+test.run()
+
+test.must_match('f2.out', "f2.in\n")
+test.must_match('f3.out', "f3.in\n")
+test.must_match('f4.out', "f4.in\n")
+test.must_match('f5.out', "f5.in\n")
+test.must_match('f6.out', "f6.in\n")
+test.must_match('f7.out', "f7.in\n")
+
+test.run(arguments='.', status=2, stderr=None)
+
+test.must_match('f1.out', "f1.in\n")
+
+test.pass_test()
diff --git a/test/silent-command.py b/test/silent-command.py
new file mode 100644 (file)
index 0000000..a5da7a8
--- /dev/null
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001, 2002, 2003, 2004 The SCons Foundation
+#
+# 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.
+#
+
+"""
+Test the use of a preceding @ to suppress printing a command.
+"""
+
+__revision__ = "/home/scons/scons/branch.0/branch.96/baseline/test/option-n.py 0.96.C352 2005/03/26 00:09:23 knight"
+
+import os
+import os.path
+import re
+import string
+import sys
+import TestCmd
+import TestSCons
+
+python = TestSCons.python
+
+test = TestSCons.TestSCons()
+
+test.subdir('build', 'src')
+
+test.write('build.py', r"""
+import sys
+fp = open(sys.argv[1], 'wb')
+for f in sys.argv[2:]:
+    fp.write(open(f, 'rb').read())
+fp.close()
+""")
+
+test.write('SConstruct', """\
+env = Environment()
+env.Command('f1.out', 'f1.in', "%(python)s build.py $TARGET $SOURCE")
+env.Command('f2.out', 'f2.in', "@%(python)s build.py $TARGET $SOURCE")
+env.Command('f3.out', 'f3.in', "@ %(python)s build.py $TARGET $SOURCE")
+env.Command('f4.out', 'f4.in', "@-%(python)s build.py $TARGET $SOURCE")
+env.Command('f5.out', 'f5.in', "@- %(python)s build.py $TARGET $SOURCE")
+env.Command('f6.out', 'f6.in', "-@%(python)s build.py $TARGET $SOURCE")
+env.Command('f7.out', 'f7.in', "-@ %(python)s build.py $TARGET $SOURCE")
+""" % locals())
+
+test.write('f1.in', "f1.in\n")
+test.write('f2.in', "f2.in\n")
+test.write('f3.in', "f3.in\n")
+test.write('f4.in', "f4.in\n")
+test.write('f5.in', "f5.in\n")
+test.write('f6.in', "f6.in\n")
+test.write('f7.in', "f7.in\n")
+
+expect = test.wrap_stdout("""\
+%(python)s build.py f1.out f1.in
+""" % locals())
+
+test.run(arguments = '.', stdout = expect)
+
+test.must_match('f1.out', "f1.in\n")
+test.must_match('f2.out', "f2.in\n")
+test.must_match('f3.out', "f3.in\n")
+test.must_match('f4.out', "f4.in\n")
+test.must_match('f5.out', "f5.in\n")
+test.must_match('f6.out', "f6.in\n")
+test.must_match('f7.out', "f7.in\n")
+
+test.pass_test()