Only execute an Action once for a List of targets.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 26 Jan 2002 13:40:32 +0000 (13:40 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 26 Jan 2002 13:40:32 +0000 (13:40 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@224 fdb21ef1-2011-0410-befe-b5e4ea1792b1

src/CHANGES.txt
src/engine/SCons/Builder.py
src/engine/SCons/BuilderTests.py
src/engine/SCons/Taskmaster.py
src/engine/SCons/TaskmasterTests.py

index cd3ebd4c277417d88079beaa8f6033f776d4f15f..f3812417e326b5e1b1fe25165155ec78606dcb69 100644 (file)
@@ -57,6 +57,12 @@ RELEASE 0.04 -
   - Remove targets before building them, and add an Environment
     Precious() method to override that.
 
+  - Eliminate redundant calls to the same builder when the target is a
+    list of targets:  Add a ListBuilder class that wraps Builders to
+    handle lists atomically.  Extend the Task class to support building
+    and updating multiple targets in a single Task.  Simplify the
+    interface between Task and Taskmaster.
+
   From Steve Leblanc:
 
   - Add var=value command-line arguments.
index 6fffeee40a246faf2d0d6bd0e99b6c9bd9b1e37e..846b286dd3ed5af93c19311c62ab828ce74671dd 100644 (file)
@@ -52,6 +52,26 @@ def Builder(**kw):
 
 
 
+def _init_nodes(builder, env, tlist, slist):
+    """Initialize lists of target and source nodes with all of
+    the proper Builder information.
+    """
+    for t in tlist:
+        t.cwd = SCons.Node.FS.default_fs.getcwd()      # XXX
+        t.builder_set(builder)
+        t.env_set(env)
+        t.add_source(slist)
+        if builder.scanner:
+            t.scanner_set(builder.scanner.instance(env))
+
+    for s in slist:
+        s.env_set(env, 1)
+        scanner = env.get_scanner(os.path.splitext(s.name)[1])
+        if scanner:
+            s.scanner_set(scanner.instance(env))
+
+
+
 class BuilderBase:
     """Base class for Builders, objects that create output
     nodes (files) from input nodes (files).
@@ -113,32 +133,17 @@ class BuilderBase:
                                            self.node_factory)
         return tlist, slist
 
-    def _init_nodes(self, env, tlist, slist):
-        """Initialize lists of target and source nodes with all of
-        the proper Builder information.
-        """
-       for t in tlist:
-            t.cwd = SCons.Node.FS.default_fs.getcwd()  # XXX
-           t.builder_set(self)
-           t.env_set(env)
-           t.add_source(slist)
-            if self.scanner:
-                t.scanner_set(self.scanner.instance(env))
-
-       for s in slist:
-           s.env_set(env, 1)
-            scanner = env.get_scanner(os.path.splitext(s.name)[1])
-            if scanner:
-                s.scanner_set(scanner.instance(env))
-
-       if len(tlist) == 1:
-           tlist = tlist[0]
-       return tlist
-
     def __call__(self, env, target = None, source = None):
         tlist, slist = self._create_nodes(env, target, source)
 
-        return self._init_nodes(env, tlist, slist)
+        if len(tlist) == 1:
+            _init_nodes(self, env, tlist, slist)
+            tlist = tlist[0]
+        else:
+            _init_nodes(ListBuilder(self, env, tlist), env, tlist, slist)
+
+        return tlist
+
 
     def execute(self, **kw):
        """Execute a builder's action to create an output object.
@@ -161,6 +166,53 @@ class BuilderBase:
             return [self.src_suffix]
         return []
 
+    def targets(self, node):
+        """Return the list of targets for this builder instance.
+
+        For most normal builders, this is just the supplied node.
+        """
+        return [ node ]
+
+class ListBuilder:
+    """This is technically not a Builder object, but a wrapper
+    around another Builder object.  This is designed to look
+    like a Builder object, though, for purposes of building an
+    array of targets from a single Action execution.
+    """
+
+    def __init__(self, builder, env, tlist):
+        self.builder = builder
+        self.scanner = builder.scanner
+        self.env = env
+        self.tlist = tlist
+
+    def execute(self, **kw):
+        if hasattr(self, 'status'):
+            return self.status
+        for t in self.tlist:
+            # unlink all targets before building any
+            t.remove()
+        kw['target'] = self.tlist[0]
+        self.status = apply(self.builder.execute, (), kw)
+        for t in self.tlist:
+            if not t is kw['target']:
+                t.build()
+        return self.status
+
+    def get_raw_contents(self, **kw):
+        return apply(self.builder.get_raw_contents, (), kw)
+
+    def get_contents(self, **kw):
+        return apply(self.builder.get_contents, (), kw)
+
+    def src_suffixes(self):
+        return self.builder.src_suffixes()
+
+    def targets(self, node):
+        """Return the list of targets for this builder instance.
+        """
+        return self.tlist
+
 class MultiStepBuilder(BuilderBase):
     """This is a builder subclass that can build targets in
     multiple steps.  The src_builder parameter to the constructor
index 65aa93471828b7e63174846c0705e5e2694d70f1..e05cdb7b4e1a30d0dc9fe62882deb702fb078ac6 100644 (file)
@@ -61,10 +61,12 @@ sys.exit(0)
 
 act_py = test.workpath('act.py')
 outfile = test.workpath('outfile')
+outfile2 = test.workpath('outfile')
 
 show_string = None
 instanced = None
 env_scanner = None
+count = 0
 
 class Environment:
     def subst(self, s):
@@ -245,14 +247,24 @@ class BuilderTestCase(unittest.TestCase):
         assert r == 0
         assert show_string == expect7, show_string
 
+        global count
+        count = 0
        def function1(**kw):
-           open(kw['target'], 'w').write("function1\n")
+            global count
+            count = count + 1
+            if not type(kw['target']) is type([]):
+                kw['target'] = [ kw['target'] ]
+            for t in kw['target']:
+               open(t, 'w').write("function1\n")
            return 1
 
        builder = MyBuilder(action = function1, name = "function1")
-       r = builder.execute(target = outfile)
-       assert r == 1
-       c = test.read(outfile, 'r')
+        r = builder.execute(target = [outfile, outfile2])
+        assert r == 1
+        assert count == 1
+        c = test.read(outfile, 'r')
+       assert c == "function1\n", c
+        c = test.read(outfile2, 'r')
        assert c == "function1\n", c
 
        class class1a:
@@ -446,6 +458,31 @@ class BuilderTestCase(unittest.TestCase):
         assert tgts[1].path == 'tgt4b.o', \
                 "Target has unexpected name: %s" % tgts[1].path
 
+    def test_ListBuilder(self):
+        """Testing ListBuilder class."""
+        global count
+        count = 0
+        def function2(**kw):
+            global count
+            count = count + 1
+            if not type(kw['target']) is type([]):
+                kw['target'] = [ kw['target'] ]
+            for t in kw['target']:
+                open(t, 'w').write("function2\n")
+            return 1
+
+        builder = SCons.Builder.Builder(action = function2, name = "function2")
+        tgts = builder(env, target = [outfile, outfile2], source = 'foo')
+        r = tgts[0].builder.execute(target = tgts[0])
+        assert r == 1, r
+        c = test.read(outfile, 'r')
+        assert c == "function2\n", c
+        c = test.read(outfile2, 'r')
+        assert c == "function2\n", c
+        r = tgts[1].builder.execute(target = tgts[1])
+        assert r == 1, r
+        assert count == 1, count
+
     def test_MultiStepBuilder(self):
         """Testing MultiStepBuilder class."""
         builder1 = SCons.Builder.Builder(name = "builder1",
index 828fecd5160f85c24671b77a2093ceb4f5c2256d..aa6fe84522e082b74e30b5a7c828b7688e74cdb1 100644 (file)
@@ -54,30 +54,33 @@ class Task:
     Note that it's generally a good idea for sub-classes to call
     these methods explicitly to update state, etc., rather than
     roll their own interaction with Taskmaster from scratch."""
-    def __init__(self, tm, target, top):
+    def __init__(self, tm, targets, top):
         self.tm = tm
-        self.target = target
+        self.targets = targets
+        self.bsig = {}
         self.top = top
 
     def execute(self):
-        if not self.target.get_state() == SCons.Node.up_to_date:
-            self.target.build()
+        if not self.targets[0].get_state() == SCons.Node.up_to_date:
+            self.targets[0].build()
 
     def get_target(self):
         """Fetch the target being built or updated by this task.
         """
-        return self.target
+        return self.targets[0]
 
-    def set_bsig(self, bsig):
-        """Set the task's (*not* the target's)  build signature.
+    def set_bsig(self, target, bsig):
+        """Set the task's (*not* the target's)  build signature
+        for this target.
 
-        This will be used later to update the target's build
-        signature if the build succeeds."""
-        self.bsig = bsig
+        This will be used later to update the target's actual
+        build signature *if* the build succeeds."""
+        self.bsig[target] = bsig
 
-    def set_tstate(self, state):
-        """Set the target node's state."""
-        self.target.set_state(state)
+    def set_tstates(self, state):
+        """Set all of the target nodes's states."""
+        for t in self.targets:
+            t.set_state(state)
 
     def executed(self):
         """Called when the task has been successfully executed.
@@ -87,10 +90,20 @@ class Task:
         things.  Most importantly, this calls back to the
         Taskmaster to put any node tasks waiting on this one
         back on the pending list."""
-        if self.target.get_state() == SCons.Node.executing:
-            self.set_tstate(SCons.Node.executed)
-            self.target.set_bsig(self.bsig)
-            self.tm.add_pending(self.target)
+        if self.targets[0].get_state() == SCons.Node.executing:
+            self.set_tstates(SCons.Node.executed)
+            for t in self.targets:
+                t.set_bsig(self.bsig[t])
+            parents = {}
+            for p in reduce(lambda x, y: x + y.get_parents(), self.targets, []):
+                parents[p] = 1
+            ready = filter(lambda x: (x.get_state() == SCons.Node.pending
+                                      and x.children_are_executed()),
+                           parents.keys())
+            tasks = {}
+            for t in map(lambda r: r.task, ready):
+                tasks[t] = 1
+            self.tm.pending_to_ready(tasks.keys())
 
     def failed(self):
         """Default action when a task fails:  stop the build."""
@@ -98,23 +111,41 @@ class Task:
 
     def fail_stop(self):
         """Explicit stop-the-build failure."""
-        self.set_tstate(SCons.Node.failed)
+        self.set_tstates(SCons.Node.failed)
         self.tm.stop()
 
     def fail_continue(self):
         """Explicit continue-the-build failure.
 
-        This sets failure status on the target node and all of
-        its dependent parent nodes.
+        This sets failure status on the target nodes and all of
+        their dependent parent nodes.
         """
-        def get_parents(node): return node.get_parents()
-        walker = SCons.Node.Walker(self.target, get_parents)
-        while 1:
-            node = walker.next()
-            if node == None: break
-            self.tm.remove_pending(node)
-            node.set_state(SCons.Node.failed)
-        
+        nodes = {}
+        for t in self.targets:
+            def get_parents(node): return node.get_parents()
+            walker = SCons.Node.Walker(t, get_parents)
+            while 1:
+                n = walker.next()
+                if n == None: break
+                nodes[n] = 1
+        pending = filter(lambda x: x.get_state() == SCons.Node.pending,
+                         nodes.keys())
+        tasks = {}
+        for t in map(lambda r: r.task, ready):
+            tasks[t] = 1
+        self.tm.pending_remove(tasks.keys())
+
+    def make_ready(self):
+        """Make a task ready for execution."""
+        state = SCons.Node.up_to_date
+        for t in self.targets:
+            bsig = self.tm.calc.bsig(t)
+            self.set_bsig(t, bsig)
+            if not self.tm.calc.current(t, bsig):
+                state = SCons.Node.executing
+        self.set_tstates(state)
+        self.tm.add_ready(self)
+
 
 
 class Calc:
@@ -196,13 +227,18 @@ class Taskmaster:
                 # and over again:
                 n.set_csig(self.calc.csig(n))
                 continue
-            task = self.tasker(self, n, self.walkers[0].is_done())
-            if not n.children_are_executed():
-                n.set_state(SCons.Node.pending)
-                n.task = task
+            try:
+                tlist = n.builder.targets(n)
+            except AttributeError:
+                tlist = [ n ]
+            task = self.tasker(self, tlist, self.walkers[0].is_done())
+            if not tlist[0].children_are_executed():
+                for t in tlist:
+                    t.set_state(SCons.Node.pending)
+                    t.task = task
                 self.pending = self.pending + 1
                 continue
-            self.make_ready(task, n)
+            task.make_ready()
             return
             
     def is_blocked(self):
@@ -214,30 +250,23 @@ class Taskmaster:
         self.pending = 0
         self.ready = []
 
-    def add_pending(self, node):
-        """Add all the pending parents that are now executable
-        to the 'ready' queue."""
-        ready = filter(lambda x: (x.get_state() == SCons.Node.pending
-                                  and x.children_are_executed()),
-                       node.get_parents())
-        for n in ready:
-            task = n.task
-            delattr(n, "task")
-            self.make_ready(task, n) 
-        self.pending = self.pending - len(ready)
-
-    def remove_pending(self, node):
-        """Remove a node from the 'ready' queue."""
-        if node.get_state() == SCons.Node.pending:
-            self.pending = self.pending - 1
-
-    def make_ready(self, task, node):
-        """Common routine that takes a single task+node and makes
-        them available on the 'ready' queue."""
-        bsig = self.calc.bsig(node)
-        task.set_bsig(bsig)
-        if self.calc.current(node, bsig):
-            task.set_tstate(SCons.Node.up_to_date)
-        else:
-            task.set_tstate(SCons.Node.executing)
+    def add_ready(self, task):
+        """Add a task to the ready queue.
+        """
         self.ready.append(task)
+
+    def pending_to_ready(self, tasks):
+        """Move the specified tasks from the pending count
+        to the 'ready' queue.
+        """
+        self.pending_remove(tasks)
+        for t in tasks:
+            t.make_ready()
+
+    def pending_remove(self, tasks):
+        """Remove tasks from the pending count.
+        
+        We assume that the caller has already confirmed that
+        the nodes in this task are in pending state.
+        """
+        self.pending = self.pending - len(tasks)
index 5e661ee380918acd5e1a6b5cac02a7bc88698c08..799f8cfce6d0783ed25450d08e644d88ca654733 100644 (file)
@@ -132,13 +132,13 @@ class TaskmasterTestCase(unittest.TestCase):
         class MyTask(SCons.Taskmaster.Task):
             def execute(self):
                 global built
-                if self.target.get_state() == SCons.Node.up_to_date:
+                if self.targets[0].get_state() == SCons.Node.up_to_date:
                     if self.top:
-                        built = self.target.name + " up-to-date top"
+                        built = self.targets[0].name + " up-to-date top"
                     else:
-                        built = self.target.name + " up-to-date"
+                        built = self.targets[0].name + " up-to-date"
                 else:
-                    self.target.build()
+                    self.targets[0].build()
 
         n1.set_state(None)
         n2.set_state(None)
@@ -303,11 +303,24 @@ class TaskmasterTestCase(unittest.TestCase):
         assert built == "MyTM.stop()"
         assert tm.next_task() is None
 
-    #def test_add_pending(self):
-    #    pass
-    #
-    #def test_remove_pending(self):
-    #    pass
+    def test_add_ready(self):
+        """Test adding a task to the ready queue"""
+        class MyTask:
+            def __init__(self, tm, tlist, top):
+                pass
+            def make_ready(self):
+                pass
+        n1 = Node("n1")
+        tm = SCons.Taskmaster.Taskmaster([n1], tasker = MyTask)
+        task = MyTask(tm, [], 0)
+        tm.add_ready(task)
+        assert tm.ready == [ task ], tm.ready
+
+    def test_pending_to_ready(self):
+        pass
+    
+    def test_pending_remove(self):
+        pass