From 68a29a4d7f4689ce6200b3e34441f2a8df9f88a0 Mon Sep 17 00:00:00 2001 From: Steven Knight Date: Tue, 26 Apr 2005 04:14:40 +0000 Subject: More flexible (and Make-like) ignoring command exit status, and suppressing printing of a command. --- doc/man/scons.1 | 58 +++++++++++++++++++++++++- src/engine/SCons/Action.py | 49 ++++++++++++++++------ src/engine/SCons/ActionTests.py | 83 +++++++++++++++++++++++++++++++++---- src/engine/SCons/Executor.py | 10 ++--- src/engine/SCons/Node/__init__.py | 8 ++-- test/ignore-command.py | 87 +++++++++++++++++++++++++++++++++++++++ test/silent-command.py | 86 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 351 insertions(+), 30 deletions(-) create mode 100644 test/ignore-command.py create mode 100644 test/silent-command.py diff --git a/doc/man/scons.1 b/doc/man/scons.1 index cb441ce..cb1334b 100644 --- a/doc/man/scons.1 +++ b/doc/man/scons.1 @@ -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 diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index 3b0230c..353ead3 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -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 diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index 3790c8c..890abb2 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -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 diff --git a/src/engine/SCons/Executor.py b/src/engine/SCons/Executor.py index afe817a..afd6c49 100644 --- a/src/engine/SCons/Executor.py +++ b/src/engine/SCons/Executor.py @@ -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__" diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 28f1c19..f9390ea 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -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 index 0000000..201c488 --- /dev/null +++ b/test/ignore-command.py @@ -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 index 0000000..a5da7a8 --- /dev/null +++ b/test/silent-command.py @@ -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() -- cgit v0.12