From 89d8f1f9283eb869b340e25072340aa7aa33d20d Mon Sep 17 00:00:00 2001 From: stevenknight Date: Sat, 13 Nov 2004 21:56:57 +0000 Subject: [PATCH] Allow Alias Nodes to have Actions. git-svn-id: http://scons.tigris.org/svn/scons/trunk@1159 fdb21ef1-2011-0410-befe-b5e4ea1792b1 --- doc/man/scons.1 | 15 ++- src/CHANGES.txt | 3 + src/engine/SCons/Environment.py | 72 ++++++---- src/engine/SCons/EnvironmentTests.py | 19 +++ src/engine/SCons/Node/Alias.py | 45 ++++--- src/engine/SCons/Node/AliasTests.py | 14 +- src/engine/SCons/Node/NodeTests.py | 13 ++ src/engine/SCons/Node/__init__.py | 7 + test/{ => Alias}/Alias.py | 12 -- test/Alias/action.py | 138 ++++++++++++++++++++ test/Alias/errors.py | 46 +++++++ test/{Alias-scanner.py => Alias/scanner.py} | 0 12 files changed, 323 insertions(+), 61 deletions(-) rename test/{ => Alias}/Alias.py (95%) create mode 100644 test/Alias/action.py create mode 100644 test/Alias/errors.py rename test/{Alias-scanner.py => Alias/scanner.py} (100%) diff --git a/doc/man/scons.1 b/doc/man/scons.1 index fdfae695..59e0d629 100644 --- a/doc/man/scons.1 +++ b/doc/man/scons.1 @@ -2266,11 +2266,17 @@ can be converted into an Action object '\""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .TP -.RI Alias( alias ", [" targets ]) +.RI Alias( alias ", [" targets ", [" action ]]) .TP -.RI env.Alias( alias ", [" targets ]) +.RI env.Alias( alias ", [" targets ", [" action ]]) Creates one or more phony targets that expand to one or more other targets. +An optional +.I action +(command) +or list of actions +can be specified that will be executed +whenever the any of the alias targets are out-of-date. Returns the Node object representing the alias, which exists outside of any file system. This Node object, or the alias name, @@ -2278,7 +2284,8 @@ may be used as a dependency of any other target, including another alias. .B Alias can be called multiple times for the same -alias to add additional targets to the alias. +alias to add additional targets to the alias, +or additional actions to the list for this alias. .ES Alias('install') @@ -2287,6 +2294,8 @@ Alias(['install', 'install-lib'], '/usr/local/lib') env.Alias('install', ['/usr/local/bin', '/usr/local/lib']) env.Alias('install', ['/usr/local/man']) + +env.Alias('update', ['file1', 'file2'], "update_database $SOURCES") .EE '\""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" diff --git a/src/CHANGES.txt b/src/CHANGES.txt index abfbd656..003163bb 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -124,6 +124,9 @@ RELEASE 0.97 - XXX to extend() list, so Python 1.5.2 doesn't get pretty-printing of Node lists, but everything should still function.) + - Allow Aliases to have actions that will be executed whenever + any of the expanded Alias targets are out of date. + From Wayne Lee: - Avoid "maximum recursion limit" errors when removing $(-$) pairs diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index 443c6b05..ef3b2d0f 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -96,6 +96,7 @@ AliasBuilder = SCons.Builder.Builder(action = alias_builder, target_factory = SCons.Node.Alias.default_ans.Alias, source_factory = SCons.Node.FS.default_fs.Entry, multi = 1, + is_explicit = None, name='AliasBuilder') def our_deepcopy(x): @@ -1036,29 +1037,56 @@ class Base: n.add_post_action(action) return nodes - def Alias(self, target, *source, **kw): - if not SCons.Util.is_List(target): - target = [target] - tlist = [] - for t in target: - if not isinstance(t, SCons.Node.Alias.Alias): - t = self.arg2nodes(self.subst(t), self.ans.Alias)[0] - tlist.append(t) - try: - s = kw['source'] - except KeyError: - try: - s = source[0] - except IndexError: - s = None - if s: - if not SCons.Util.is_List(s): - s = [s] - s = filter(None, s) - s = self.arg2nodes(s, self.fs.Entry) + def Alias(self, target, source=[], action=None, **kw): + tlist = self.arg2nodes(target, self.ans.Alias) + if not SCons.Util.is_List(source): + source = [source] + source = filter(None, source) + + if not action: + if not source: + # There are no source files and no action, so just + # return a target list of classic Alias Nodes, without + # any builder. The externally visible effect is that + # this will make the wrapping Script.BuildTask class + # say that there's "Nothing to be done" for this Alias, + # instead of that it's "up to date." + return tlist + + # No action, but there are sources. Re-call all the target + # builders to add the sources to each target. + result = [] for t in tlist: - AliasBuilder(self, t, s) - return tlist + bld = t.get_builder(AliasBuilder) + result.extend(bld(self, t, source)) + return result + + action = SCons.Action.Action(action) + nkw = self.subst_kw(kw) + nkw['source_factory'] = self.fs.Entry + nkw['multi'] = 1 + nkw['action'] = action + bld = apply(SCons.Builder.Builder, (), nkw) + + # Apply the Builder separately to each target so that the Aliases + # stay separate. If we did one "normal" Builder call with the + # whole target list, then all of the target Aliases would be + # associated under a single Executor. + result = [] + for t in tlist: + # Calling the convert() method will cause a new Executor to be + # created from scratch, so we have to explicitly initialize + # it with the target's existing sources, plus our new ones, + # so nothing gets lost. + b = t.get_builder() + if b is None or b is AliasBuilder: + b = bld + else: + nkw['action'] = b.action + action + b = apply(SCons.Builder.Builder, (), nkw) + t.convert() + result.extend(b(self, t, t.sources + source)) + return result def AlwaysBuild(self, *targets): tlist = [] diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py index 0a0f260d..9b84f3c8 100644 --- a/src/engine/SCons/EnvironmentTests.py +++ b/src/engine/SCons/EnvironmentTests.py @@ -1797,6 +1797,7 @@ f5: \ tgt = env.Alias('new_alias')[0] assert str(tgt) == 'new_alias', tgt assert tgt.sources == [], tgt.sources + assert not hasattr(tgt, 'builder'), tgt.builder tgt = env.Alias('None_alias', None)[0] assert str(tgt) == 'None_alias', tgt @@ -1833,6 +1834,24 @@ f5: \ assert str(t2.sources[0]) == 'asrc6', map(str, t2.sources) assert str(t2.sources[1]) == 'asrc7', map(str, t2.sources) + tgt = env.Alias('add', 's1') + tgt = env.Alias('add', 's2')[0] + s = map(str, tgt.sources) + assert s == ['s1', 's2'], s + tgt = env.Alias(tgt, 's3')[0] + s = map(str, tgt.sources) + assert s == ['s1', 's2', 's3'], s + + tgt = env.Alias('act', None, "action1")[0] + s = str(tgt.builder.action) + assert s == "action1", s + tgt = env.Alias('act', None, "action2")[0] + s = str(tgt.builder.action) + assert s == "action1\naction2", s + tgt = env.Alias(tgt, None, "action3")[0] + s = str(tgt.builder.action) + assert s == "action1\naction2\naction3", s + def test_AlwaysBuild(self): """Test the AlwaysBuild() method""" env = Environment(FOO='fff', BAR='bbb') diff --git a/src/engine/SCons/Node/Alias.py b/src/engine/SCons/Node/Alias.py index 89127a30..c3618382 100644 --- a/src/engine/SCons/Node/Alias.py +++ b/src/engine/SCons/Node/Alias.py @@ -39,11 +39,15 @@ import SCons.Node import SCons.Util class AliasNameSpace(UserDict.UserDict): - def Alias(self, name): - if self.has_key(name): - raise SCons.Errors.UserError - self[name] = SCons.Node.Alias.Alias(name) - return self[name] + def Alias(self, name, **kw): + if isinstance(name, SCons.Node.Alias.Alias): + return name + try: + a = self[name] + except KeyError: + a = apply(SCons.Node.Alias.Alias, (name,), kw) + self[name] = a + return a def lookup(self, name): try: @@ -59,18 +63,11 @@ class Alias(SCons.Node.Node): def __str__(self): return self.name - def build(self): - """A "builder" for aliases.""" - pass - + really_build = SCons.Node.Node.build current = SCons.Node.Node.children_are_up_to_date - def sconsign(self): - """An Alias is not recorded in .sconsign files""" - pass - def is_under(self, dir): - # Make Alias nodes get built regardless of + # Make Alias nodes get built regardless of # what directory scons was run from. Alias nodes # are outside the filesystem: return 1 @@ -82,7 +79,25 @@ class Alias(SCons.Node.Node): for kid in self.children(None): contents = contents + kid.get_contents() return contents - + + def sconsign(self): + """An Alias is not recorded in .sconsign files""" + pass + + # + # + # + + def build(self): + """A "builder" for aliases.""" + pass + + def convert(self): + try: del self.builder + except AttributeError: pass + self.reset_executor() + self.build = self.really_build + default_ans = AliasNameSpace() SCons.Node.arg2nodes_lookups.append(default_ans.lookup) diff --git a/src/engine/SCons/Node/AliasTests.py b/src/engine/SCons/Node/AliasTests.py index afae46b6..417cc2b3 100644 --- a/src/engine/SCons/Node/AliasTests.py +++ b/src/engine/SCons/Node/AliasTests.py @@ -42,15 +42,11 @@ class AliasTestCase(unittest.TestCase): """ ans = SCons.Node.Alias.AliasNameSpace() - a = ans.Alias('a1') - assert a.name == 'a1', a.name - - try: - ans.Alias('a1') - except SCons.Errors.UserError: - pass - else: - raise TestFailed, "did not catch expected UserError" + a1 = ans.Alias('a1') + assert a1.name == 'a1', a1.name + + a2 = ans.Alias('a1') + assert a1 is a2, (a1, a2) def test_lookup(self): """Test the lookup() method diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py index 657ac9b1..e0956e10 100644 --- a/src/engine/SCons/Node/NodeTests.py +++ b/src/engine/SCons/Node/NodeTests.py @@ -390,6 +390,19 @@ class NodeTestCase(unittest.TestCase): n1.builder_set(Builder(is_explicit=None)) assert not n1.has_explicit_builder() + def test_get_builder(self): + """Test the get_builder() method""" + n1 = SCons.Node.Node() + b = n1.get_builder() + assert b is None, b + b = n1.get_builder(777) + assert b == 777, b + n1.builder_set(888) + b = n1.get_builder() + assert b == 888, b + b = n1.get_builder(999) + assert b == 888, b + def test_multiple_side_effect_has_builder(self): """Test the multiple_side_effect_has_builder() method """ diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 36b0aeda..fc3d7ec4 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -329,6 +329,13 @@ class Node: directories).""" return self.has_builder() and self.builder.is_explicit + def get_builder(self, default_builder=None): + """Return the set builder, or a specified default value""" + try: + return self.builder + except AttributeError: + return default_builder + multiple_side_effect_has_builder = has_builder def is_derived(self): diff --git a/test/Alias.py b/test/Alias/Alias.py similarity index 95% rename from test/Alias.py rename to test/Alias/Alias.py index 1facd28b..56efafdc 100644 --- a/test/Alias.py +++ b/test/Alias/Alias.py @@ -166,16 +166,4 @@ test.run(arguments = 'f1.out', test.up_to_date(arguments = 'f1.out') -test.write('SConstruct', """ -env=Environment() -TargetSignatures('content') -env.Alias('C', 'D') -env.Alias('B', 'C') -env.Alias('A', 'B') -""") - -test.run(arguments='A', - stderr="scons: \\*\\*\\* Source `D' not found, needed by target `C'. Stop.\n", - status=2) - test.pass_test() diff --git a/test/Alias/action.py b/test/Alias/action.py new file mode 100644 index 00000000..ea0eac2d --- /dev/null +++ b/test/Alias/action.py @@ -0,0 +1,138 @@ +#!/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. +# + +""" +Test that Aliases with actions work. +""" + +__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" + +import os +import sys +import TestSCons +import TestCmd + +test = TestSCons.TestSCons() + +test.write('SConstruct', """ +def cat(target, source, env): + target = str(target[0]) + source = map(str, source) + f = open(target, "wb") + for src in source: + f.write(open(src, "rb").read()) + f.close() + +def foo(target, source, env): + target = map(str, target) + source = map(str, source) + open('foo', 'wb').write("foo(%s, %s)\\n" % (target, source)) + +def bar(target, source, env): + target = map(str, target) + source = map(str, source) + open('bar', 'wb').write("bar(%s, %s)\\n" % (target, source)) + +env = Environment(BUILDERS = {'Cat':Builder(action=cat)}) +env.Alias(target = ['build-f1'], source = 'f1.out', action = foo) +f1 = env.Cat('f1.out', 'f1.in') +f2 = env.Cat('f2.out', 'f2.in') +f3 = env.Cat('f3.out', 'f3.in') +f4 = env.Cat('f4.out', 'f4.in') +f5 = env.Cat('f5.out', 'f5.in') +f6 = env.Cat('f6.out', 'f6.in') +env.Alias('build-all', [f1, f2, f3], foo) +env.Alias('build-add1', f3, foo) +env.Alias('build-add1', f2) +env.Alias('build-add2a', f4) +env.Alias('build-add2b', f5) +env.Alias(['build-add2a', 'build-add2b'], action=foo) +env.Alias('build-add3', f6) +env.Alias('build-add3', action=foo) +env.Alias('build-add3', action=bar) +""") + +test.write('f1.in', "f1.in 1\n") +test.write('f2.in', "f2.in 1\n") +test.write('f3.in', "f3.in 1\n") +test.write('f4.in', "f4.in 1\n") +test.write('f5.in', "f5.in 1\n") +test.write('f6.in', "f6.in 1\n") + +test.run(arguments = 'build-f1') + +test.must_match('f1.out', "f1.in 1\n") +test.must_match('foo', "foo(['build-f1'], ['f1.out'])\n") + +test.up_to_date(arguments = 'build-f1') + +test.write('f1.in', "f1.in 2\n") +test.unlink('foo') + +test.run(arguments = 'build-f1') + +test.must_match('f1.out', "f1.in 2\n") +test.must_match('foo', "foo(['build-f1'], ['f1.out'])\n") + +test.run(arguments = 'build-all') + +test.must_match('f1.out', "f1.in 2\n") +test.must_match('f2.out', "f2.in 1\n") +test.must_match('f3.out', "f3.in 1\n") +test.must_match('foo', "foo(['build-all'], ['f1.out', 'f2.out', 'f3.out'])\n") + +test.up_to_date(arguments = 'build-all') +test.up_to_date(arguments = 'build-add1') + +test.write('f1.in', "f1.in 3\n") +test.write('f3.in', "f3.in 2\n") +test.unlink('foo') + +test.run(arguments = 'build-add1') + +test.must_match('f1.out', "f1.in 2\n") +test.must_match('f2.out', "f2.in 1\n") +test.must_match('f3.out', "f3.in 2\n") +test.must_match('foo', "foo(['build-add1'], ['f3.out', 'f2.out'])\n") + +test.up_to_date(arguments = 'build-add1') + +test.run(arguments = 'build-add2a') + +test.must_match('f4.out', "f4.in 1\n") +test.must_not_exist('f5.out') +test.must_match('foo', "foo(['build-add2a'], ['f4.out'])\n") + +test.run(arguments = 'build-add2b') + +test.must_match('f5.out', "f5.in 1\n") +test.must_match('foo', "foo(['build-add2b'], ['f5.out'])\n") + +test.run(arguments = 'build-add3') + +test.must_match('f6.out', "f6.in 1\n") +test.must_match('foo', "foo(['build-add3'], ['f6.out'])\n") +test.must_match('bar', "bar(['build-add3'], ['f6.out'])\n") + +test.pass_test() diff --git a/test/Alias/errors.py b/test/Alias/errors.py new file mode 100644 index 00000000..f2ca950f --- /dev/null +++ b/test/Alias/errors.py @@ -0,0 +1,46 @@ +#!/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 os +import sys +import TestSCons +import TestCmd + +test = TestSCons.TestSCons() + +test.write('SConstruct', """ +env=Environment() +TargetSignatures('content') +env.Alias('C', 'D') +env.Alias('B', 'C') +env.Alias('A', 'B') +""") + +test.run(arguments='A', + stderr="scons: *** Source `D' not found, needed by target `C'. Stop.\n", + status=2) + +test.pass_test() diff --git a/test/Alias-scanner.py b/test/Alias/scanner.py similarity index 100% rename from test/Alias-scanner.py rename to test/Alias/scanner.py -- 2.26.2