summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteven Knight <knight@baldmt.com>2002-01-26 13:40:32 (GMT)
committerSteven Knight <knight@baldmt.com>2002-01-26 13:40:32 (GMT)
commite9a2f2b02f67bf88917d74c9725f8d72b86b7cfc (patch)
treea49960ab7d5b30717957d2e3f2be184e65eecc8a
parentca75f94e83c37ca03de9740bb141e442c53db235 (diff)
downloadSCons-e9a2f2b02f67bf88917d74c9725f8d72b86b7cfc.zip
SCons-e9a2f2b02f67bf88917d74c9725f8d72b86b7cfc.tar.gz
SCons-e9a2f2b02f67bf88917d74c9725f8d72b86b7cfc.tar.bz2
Only execute an Action once for a List of targets.
-rw-r--r--src/CHANGES.txt6
-rw-r--r--src/engine/SCons/Builder.py98
-rw-r--r--src/engine/SCons/BuilderTests.py45
-rw-r--r--src/engine/SCons/Taskmaster.py147
-rw-r--r--src/engine/SCons/TaskmasterTests.py31
5 files changed, 232 insertions, 95 deletions
diff --git a/src/CHANGES.txt b/src/CHANGES.txt
index cd3ebd4..f381241 100644
--- a/src/CHANGES.txt
+++ b/src/CHANGES.txt
@@ -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.
diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py
index 6fffeee..846b286 100644
--- a/src/engine/SCons/Builder.py
+++ b/src/engine/SCons/Builder.py
@@ -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
diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py
index 65aa934..e05cdb7 100644
--- a/src/engine/SCons/BuilderTests.py
+++ b/src/engine/SCons/BuilderTests.py
@@ -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",
diff --git a/src/engine/SCons/Taskmaster.py b/src/engine/SCons/Taskmaster.py
index 828fecd..aa6fe84 100644
--- a/src/engine/SCons/Taskmaster.py
+++ b/src/engine/SCons/Taskmaster.py
@@ -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)
diff --git a/src/engine/SCons/TaskmasterTests.py b/src/engine/SCons/TaskmasterTests.py
index 5e661ee..799f8cf 100644
--- a/src/engine/SCons/TaskmasterTests.py
+++ b/src/engine/SCons/TaskmasterTests.py
@@ -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