Allow Alias Nodes to have Actions.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 13 Nov 2004 21:56:57 +0000 (21:56 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 13 Nov 2004 21:56:57 +0000 (21:56 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@1159 fdb21ef1-2011-0410-befe-b5e4ea1792b1

12 files changed:
doc/man/scons.1
src/CHANGES.txt
src/engine/SCons/Environment.py
src/engine/SCons/EnvironmentTests.py
src/engine/SCons/Node/Alias.py
src/engine/SCons/Node/AliasTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
test/Alias/Alias.py [moved from test/Alias.py with 95% similarity]
test/Alias/action.py [new file with mode: 0644]
test/Alias/errors.py [new file with mode: 0644]
test/Alias/scanner.py [moved from test/Alias-scanner.py with 100% similarity]

index fdfae695de5c4f0fe77c635998cc609069d71261..59e0d629df72728407e0711d44da9e13b8d84837 100644 (file)
@@ -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
 
 '\"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
index abfbd65622011343641bad9431ce3438701ab2e0..003163bb9b44fd8a7856b92934861ad1c4bac22c 100644 (file)
@@ -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
index 443c6b050eb87dd1b561ed972137eaed985a91b9..ef3b2d0ff35aec0d782138073b328fe1342b26c3 100644 (file)
@@ -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 = []
index 0a0f260d2c0e70c7874ed3c214117cb59f43b0d1..9b84f3c83fc93910b1d4f8905b865382af17c206 100644 (file)
@@ -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')
index 89127a30095ca49682ed025f4e0b5fc963102d61..c3618382d6c242508e48b61f3dad054872c3aee6 100644 (file)
@@ -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)
index afae46b68b4176f23d997a85db4230bf93e065ce..417cc2b38d5c0b8d7e1cce454b3c767ee6e0a02d 100644 (file)
@@ -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
index 657ac9b1ad3cb534bd71767f61b79a032a921622..e0956e10530343cb12fc8a3081db819a5fec8c71 100644 (file)
@@ -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
         """
index 36b0aeda1e940089d505be911747c84e50b813c2..fc3d7ec466e9a57d15bb0e5a9ed9be4b88029e8f 100644 (file)
@@ -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):
similarity index 95%
rename from test/Alias.py
rename to test/Alias/Alias.py
index 1facd28b2990da1a081a0ea128640da31e26327d..56efafdc3d76316848507b319f3522c2f1567132 100644 (file)
@@ -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 (file)
index 0000000..ea0eac2
--- /dev/null
@@ -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 (file)
index 0000000..f2ca950
--- /dev/null
@@ -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()
similarity index 100%
rename from test/Alias-scanner.py
rename to test/Alias/scanner.py