Officially support target_factory and source_factory when creating a Builder.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 25 Jun 2004 04:10:24 +0000 (04:10 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 25 Jun 2004 04:10:24 +0000 (04:10 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@995 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/CHANGES.txt
src/engine/SCons/Action.py
src/engine/SCons/ActionTests.py
src/engine/SCons/Builder.py
src/engine/SCons/Environment.py
src/engine/SCons/Node/FS.py
src/engine/SCons/Node/FSTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
test/Builder-factories.py [new file with mode: 0644]

index 63c6a83eff604d4b01cd877ee33d3e54302b2e11..947804f6c8f7793c8fb1ead929689f3efa21f11f 100644 (file)
@@ -6288,8 +6288,25 @@ opts.AddOptions(
 .SH EXTENDING SCONS
 .SS Builder Objects
 .B scons
-can be extended by adding new builders to a construction
-environment using the 
+can be extended to build different types of targets
+by adding new Builder objects
+to a construction environment.
+.IR "In general" ,
+you should only need to add a new Builder object
+when you want to build a new type of file or other external target.
+If you just want to invoke a different compiler or other tool
+to build a Program, Object, Library, or any other
+type of output file for which
+.B scons
+already has an existing Builder,
+it is generally much easier to
+use those existing Builders
+in a construction environment
+that sets the appropriate construction variables
+(CC, LINK, etc.).
+
+Builder objects are created
+using the 
 .B Builder 
 function.
 The
@@ -6391,12 +6408,58 @@ lines in source files.
 (See the section "Scanner Objects," below,
 for information about creating Scanner objects.)
 
+.IP target_factory
+A factory function that the Builder will use
+to turn any targets specified as strings into SCons Nodes.
+By default,
+SCons assumes that all targets are files.
+Other useful target_factory
+values include
+.BR Dir ,
+for when a Builder creates a directory target,
+and
+.BR Entry ,
+for when a Builder can create either a file
+or directory target.
+
+Example:
+
+.ES
+MakeDirectoryBuilder = Builder(action=my_mkdir, target_factory=Dir)
+env = Environment()
+env.Append(BUILDERS = {'MakeDirectory':MakeDirectoryBuilder})
+env.MakeDirectory('new_directory')
+.EE
+
+.IP source_factory
+A factory function that the Builder will use
+to turn any sources specified as strings into SCons Nodes.
+By default,
+SCons assumes that all source are files.
+Other useful source_factory
+values include
+.BR Dir ,
+for when a Builder uses a directory as a source,
+and
+.BR Entry ,
+for when a Builder can use files
+or directories (or both) as sources.
+
+Example:
+
+.ES
+CollectBuilder = Builder(action=my_mkdir, source_factory=Entry)
+env = Environment()
+env.Append(BUILDERS = {'Collect':CollectBuilder})
+env.Collect('archive', ['directory_name', 'file_name'])
+.EE
+
 .IP emitter
 A function or list of functions to manipulate the target and source
 lists before dependencies are established
 and the target(s) are actually built.
 .B emitter
-can also be string containing a construction variable to expand
+can also be string containing a construction variable to expand
 to an emitter function or list of functions,
 or a dictionary mapping source file suffixes
 to emitter functions.
@@ -6453,6 +6516,12 @@ b = Builder("my_build < $TARGET > $SOURCE",
             emitter = {'.suf1' : e_suf1,
                        '.suf2' : e_suf2})
 .EE
+.IP
+The 
+.I generator
+and
+.I action
+arguments must not both be used for the same Builder.
 
 .IP multi
 Specifies whether this builder is allowed to be called multiple times for
@@ -6508,12 +6577,29 @@ any of the suffixes of the builder. Using this argument produces a
 multi-stage builder.
 
 .RE
+.IP
 The 
 .I generator
 and
 .I action
 arguments must not both be used for the same Builder.
 
+.IP env
+A construction environment that can be used
+to fetch source code using this Builder.
+(Note that this environment is
+.I not
+used for normal builds of normal target files,
+which use the environment that was
+used to call the Builder for the target file.)
+
+.ES
+b = Builder(action="build < $SOURCE > $TARGET")
+env = Environment(BUILDERS = {'MyBuild' : b})
+env.MyBuild('foo.out', 'foo.in', my_arg = 'xyzzy')
+.EE
+
+.RE
 Any additional keyword arguments supplied
 when a Builder object is created
 (that is, when the Builder() function is called)
@@ -6532,12 +6618,6 @@ created by that particular Builder call
 (and any other files built as a
 result of the call).
 
-.ES
-b = Builder(action="build < $SOURCE > $TARGET")
-env = Environment(BUILDERS = {'MyBuild' : b})
-env.MyBuild('foo.out', 'foo.in', my_arg = 'xyzzy')
-.EE
-
 These extra keyword arguments are passed to the
 following functions:
 command generator functions,
index ee758573e3f40e5cdc5bb74fa93167044dd52f00..f2ee12dcdfd8339fac1f4af2bd18175b25ee4db1 100644 (file)
@@ -138,6 +138,10 @@ RELEASE 0.96 - XXX
   - Slim down the internal Sig.Calculator class by eliminating methods
     whose functionality is now covered by Node methods.
 
+  - Document use of the target_factory and source_factory keyword
+    arguments when creating Builder objects.  Enhance Dir Nodes so that
+    they can be created with user-specified Builder objects.
+
   From Gary Oberbrunner:
 
   - Add a --debug=presub option to print actions prior to substitution.
index c8d62cd71b03ce93b2b5d2b8a2f74289cb9746a1..6a90c7696ebf818e1224316a53a58aac38864d29 100644 (file)
@@ -191,25 +191,29 @@ def _do_create_action(act, *args, **kw):
             return apply(ListAction, (listCmdActions,)+args, kw)
     return None
 
-def Action(act, strfunction=_null, varlist=[]):
+def Action(act, strfunction=_null, varlist=[], presub=_null):
     """A factory for action objects."""
     if SCons.Util.is_List(act):
-        acts = map(lambda x, s=strfunction, v=varlist:
-                          _do_create_action(x, strfunction=s, varlist=v),
+        acts = map(lambda x, s=strfunction, v=varlist, ps=presub:
+                          _do_create_action(x, strfunction=s, varlist=v, presub=ps),
                    act)
         acts = filter(lambda x: not x is None, acts)
         if len(acts) == 1:
             return acts[0]
         else:
-            return ListAction(acts, strfunction=strfunction, varlist=varlist)
+            return ListAction(acts, strfunction=strfunction, varlist=varlist, presub=presub)
     else:
-        return _do_create_action(act, strfunction=strfunction, varlist=varlist)
+        return _do_create_action(act, strfunction=strfunction, varlist=varlist, presub=presub)
 
 class ActionBase:
     """Base class for actions that create output objects."""
-    def __init__(self, strfunction=_null, **kw):
+    def __init__(self, strfunction=_null, presub=_null, **kw):
         if not strfunction is _null:
             self.strfunction = strfunction
+        if presub is _null:
+            self.presub = print_actions_presub
+        else:
+            self.presub = presub
 
     def __cmp__(self, other):
         return cmp(self.__dict__, other.__dict__)
@@ -223,12 +227,12 @@ class ActionBase:
             target = [target]
         if not SCons.Util.is_List(source):
             source = [source]
-        if presub is _null:  presub = print_actions_presub
+        if presub is _null:  presub = self.presub
         if show is _null:  show = print_actions
         if execute is _null:  execute = execute_actions
         if presub:
             t = string.join(map(str, target), 'and')
-            l = string.join(self.presub(env), '\n  ')
+            l = string.join(self.presub_lines(env), '\n  ')
             out = "Building %s with action(s):\n  %s\n" % (t, l)
             sys.stdout.write(out)
         if show and self.strfunction:
@@ -243,7 +247,7 @@ class ActionBase:
         else:
             return 0
 
-    def presub(self, env):
+    def presub_lines(self, env):
         # CommandGeneratorAction needs a real environment
         # in order to return the proper string here, since
         # it may call LazyCmdGenerator, which looks up a key
index f699e61fa8c9f9e79eeae3c6c27badc79cc68ed4..9a8962338341c6d39f98337f10f363a0d0dba5ca 100644 (file)
@@ -392,11 +392,27 @@ class ActionBaseTestCase(unittest.TestCase):
             result = a("out", "in", env)
             assert result == 0, result
             s = sio.getvalue()
+            assert s == 'execfunc("out", "in")\n', s
+
+            sio = StringIO.StringIO()
+            sys.stdout = sio
+            result = a("out", "in", env, presub=1)
+            assert result == 0, result
+            s = sio.getvalue()
+            assert s == 'Building out with action(s):\n  execfunc(env, target, source)\nexecfunc("out", "in")\n', s
+
+            a2 = SCons.Action.Action(execfunc)
+
+            sio = StringIO.StringIO()
+            sys.stdout = sio
+            result = a2("out", "in", env)
+            assert result == 0, result
+            s = sio.getvalue()
             assert s == 'Building out with action(s):\n  execfunc(env, target, source)\nexecfunc("out", "in")\n', s
 
             sio = StringIO.StringIO()
             sys.stdout = sio
-            result = a("out", "in", env, presub=0)
+            result = a2("out", "in", env, presub=0)
             assert result == 0, result
             s = sio.getvalue()
             assert s == 'execfunc("out", "in")\n', s
@@ -428,36 +444,36 @@ class ActionBaseTestCase(unittest.TestCase):
             SCons.Action.print_actions_presub = save_print_actions_presub
             SCons.Action.execute_actions = save_execute_actions
 
-    def test_presub(self):
-        """Test the presub() method
+    def test_presub_lines(self):
+        """Test the presub_lines() method
         """
         env = Environment()
         a = SCons.Action.Action("x")
-        s = a.presub(env)
+        s = a.presub_lines(env)
         assert s == ['x'], s
 
         a = SCons.Action.Action(["y", "z"])
-        s = a.presub(env)
+        s = a.presub_lines(env)
         assert s == ['y', 'z'], s
 
         def func():
             pass
         a = SCons.Action.Action(func)
-        s = a.presub(env)
+        s = a.presub_lines(env)
         assert s == ["func(env, target, source)"], s
 
         def gen(target, source, env, for_signature):
             return 'generat' + env.get('GEN', 'or')
         a = SCons.Action.Action(SCons.Action.CommandGenerator(gen))
-        s = a.presub(env)
+        s = a.presub_lines(env)
         assert s == ["generator"], s
-        s = a.presub(Environment(GEN = 'ed'))
+        s = a.presub_lines(Environment(GEN = 'ed'))
         assert s == ["generated"], s
 
         a = SCons.Action.Action("$ACT")
-        s = a.presub(env)
+        s = a.presub_lines(env)
         assert s == [''], s
-        s = a.presub(Environment(ACT = 'expanded action'))
+        s = a.presub_lines(Environment(ACT = 'expanded action'))
         assert s == ['expanded action'], s
 
     def test_get_actions(self):
index 733a26a13671ee5b85999a99ecc404c3392d0c00..1dcf84c26fe848e88b52b34b0542467e8d0c9b9b 100644 (file)
@@ -285,7 +285,7 @@ def _init_nodes(builder, env, overrides, tlist, slist):
         if t.side_effect:
             raise UserError, "Multiple ways to build the same target were specified for: %s" % str(t)
         if t.has_builder():
-            if not t.env is env:
+            if not t.env is None and not t.env is env:
                 t_contents = t.builder.action.get_contents(tlist, slist, t.env)
                 contents = t.builder.action.get_contents(tlist, slist, env)
 
index 576652381677c5d530fc0d97bd602e264993e405..080232ab4c8190e62b4e84fc77df07cf5c8829a1 100644 (file)
@@ -1172,10 +1172,7 @@ class Base:
         targets = self.arg2nodes(target, self.fs.Entry)
 
         for side_effect in side_effects:
-            # A builder of 1 means the node is supposed to appear
-            # buildable without actually having a builder, so we allow
-            # it to be a side effect as well.
-            if side_effect.has_builder() and side_effect.builder != 1:
+            if side_effect.multiple_side_effect_has_builder():
                 raise SCons.Errors.UserError, "Multiple ways to build the same target were specified for: %s" % str(side_effect)
             side_effect.add_source(targets)
             side_effect.side_effect = 1
index 2f115c9c787fa97121977532fad50d64820efb12..52a75ce5a0ffe69b717dce40bd22d0e6c2ebdabb 100644 (file)
@@ -176,10 +176,24 @@ Unlink = SCons.Action.Action(UnlinkFunc, None)
 
 def MkdirFunc(target, source, env):
     t = target[0]
-    t.fs.mkdir(t.path)
+    if not t.fs.exists(t.path):
+        t.fs.mkdir(t.path)
     return 0
 
-Mkdir = SCons.Action.Action(MkdirFunc, None)
+Mkdir = SCons.Action.Action(MkdirFunc, None, presub=None)
+
+MkdirBuilder = None
+
+def get_MkdirBuilder():
+    global MkdirBuilder
+    if MkdirBuilder is None:
+        import SCons.Builder
+        import SCons.Defaults
+        env = SCons.Defaults.DefaultEnvironment()
+        MkdirBuilder = SCons.Builder.Builder(action = Mkdir,
+                                             env = env,
+                                             explain = None)
+    return MkdirBuilder
 
 def CacheRetrieveFunc(target, source, env):
     t = target[0]
@@ -1118,7 +1132,7 @@ class Dir(Base):
         self.entries['.'] = self
         self.entries['..'] = self.dir
         self.cwd = self
-        self.builder = 1
+        self.builder = get_MkdirBuilder()
         self.searched = 0
         self._sconsign = None
         self.build_dirs = []
@@ -1238,7 +1252,13 @@ class Dir(Base):
 
     def build(self, **kw):
         """A null "builder" for directories."""
-        pass
+        global MkdirBuilder
+        if not self.builder is MkdirBuilder:
+            apply(SCons.Node.Node.build, [self,], kw)
+
+    def multiple_side_effect_has_builder(self):
+        global MkdirBuilder
+        return not self.builder is MkdirBuilder and self.has_builder()
 
     def alter_targets(self):
         """Return any corresponding targets in a build directory.
@@ -1262,6 +1282,8 @@ class Dir(Base):
     def current(self, calc=None):
         """If all of our children were up-to-date, then this
         directory was up-to-date, too."""
+        if not self.builder is MkdirBuilder and not self.exists():
+            return 0
         state = 0
         for kid in self.children():
             s = kid.get_state()
@@ -1299,16 +1321,6 @@ class Dir(Base):
             return self.srcdir
         return Base.srcnode(self)
 
-    def get_executor(self, create=1):
-        """Fetch the action executor for this node.  Create one if
-        there isn't already one, and requested to do so."""
-        try:
-            executor = self.executor
-        except AttributeError:
-            executor = DummyExecutor()
-            self.executor = executor
-        return executor
-
     def get_timestamp(self):
         """Return the latest timestamp from among our children"""
         stamp = 0
@@ -1482,8 +1494,12 @@ class File(Base):
         listDirs.reverse()
         for dirnode in listDirs:
             try:
-                Mkdir(dirnode, [], None)
-                # The Mkdir() action may or may not have actually
+                # Don't call dirnode.build(), call the base Node method
+                # directly because we definitely *must* create this
+                # directory.  The dirnode.build() method will suppress
+                # the build if it's the default builder.
+                SCons.Node.Node.build(dirnode)
+                # The build() action may or may not have actually
                 # created the directory, depending on whether the -n
                 # option was used or not.  Delete the _exists and
                 # _rexists attributes so they can be reevaluated.
index d4137c1c75bdfa9d1794db2f083fc04e545ea5ae..bbc64ef9c713d7dcb9ae6b41ae79772d00b07496 100644 (file)
@@ -281,8 +281,33 @@ class BuildDirTestCase(unittest.TestCase):
                f8.rfile().path
 
         # Verify the Mkdir and Link actions are called
+        d9 = fs.Dir('build/var2/new_dir')
         f9 = fs.File('build/var2/new_dir/test9.out')
 
+        class MkdirAction(Action):
+            def __init__(self, dir_made):
+                self.dir_made = dir_made
+            def __call__(self, target, source, env, errfunc):
+                self.dir_made.extend(target)
+
+        save_Link = SCons.Node.FS.Link
+        link_made = []
+        def link_func(target, source, env, link_made=link_made):
+            link_made.append(target)
+        SCons.Node.FS.Link = link_func
+
+        try:
+            dir_made = []
+            d9.builder = Builder(fs.Dir, action=MkdirAction(dir_made))
+            f9.exists()
+            expect = os.path.join('build', 'var2', 'new_dir')
+            assert dir_made[0].path == expect, dir_made[0].path
+            expect = os.path.join('build', 'var2', 'new_dir', 'test9.out')
+            assert link_made[0].path == expect, link_made[0].path
+            assert f9.linked
+        finally:
+            SCons.Node.FS.Link = save_Link
+
         # Test for an interesting pathological case...we have a source
         # file in a build path, but not in a source path.  This can
         # happen if you switch from duplicate=1 to duplicate=0, then
@@ -312,29 +337,6 @@ class BuildDirTestCase(unittest.TestCase):
         var2_new_dir = os.path.normpath('build/var2/new_dir')
         assert bdt == [var1_new_dir, var2_new_dir], bdt
 
-        save_Mkdir = SCons.Node.FS.Mkdir
-        dir_made = []
-        def mkdir_func(target, source, env, dir_made=dir_made):
-            dir_made.append(target)
-        SCons.Node.FS.Mkdir = mkdir_func
-
-        save_Link = SCons.Node.FS.Link
-        link_made = []
-        def link_func(target, source, env, link_made=link_made):
-            link_made.append(target)
-        SCons.Node.FS.Link = link_func
-
-        try:
-            f9.exists()
-            expect = os.path.join('build', 'var2', 'new_dir')
-            assert dir_made[0].path == expect, dir_made[0].path
-            expect = os.path.join('build', 'var2', 'new_dir', 'test9.out')
-            assert link_made[0].path == expect, link_made[0].path
-            assert f9.linked
-        finally:
-            SCons.Node.FS.Mkdir = save_Mkdir
-            SCons.Node.FS.Link = save_Link
-
         # Test that an IOError trying to Link a src file
         # into a BuildDir ends up throwing a StopError.
         fIO = fs.File("build/var2/IOError")
@@ -754,7 +756,7 @@ class FSTestCase(unittest.TestCase):
         d1.builder_set(Builder(fs.File))
         d1.env_set(Environment())
         d1.build()
-        assert not built_it
+        assert built_it
 
         built_it = None
         assert not built_it
@@ -1497,22 +1499,24 @@ class prepareTestCase(unittest.TestCase):
             exc_caught = 1
         assert exc_caught, "Should have caught a StopError."
 
-        save_Mkdir = SCons.Node.FS.Mkdir
-        dir_made = []
-        def mkdir_func(target, source, env, dir_made=dir_made):
-            dir_made.append(target)
-        SCons.Node.FS.Mkdir = mkdir_func
+        class MkdirAction(Action):
+            def __init__(self, dir_made):
+                self.dir_made = dir_made
+            def __call__(self, target, source, env, errfunc):
+                self.dir_made.extend(target)
 
-        file = fs.File(os.path.join("new_dir", "xyz"))
-        try:
-            file.set_state(SCons.Node.up_to_date)
-            file.prepare()
-            assert dir_made == [], dir_made
-            file.set_state(0)
-            file.prepare()
-            assert dir_made[0].path == "new_dir", dir_made[0].path
-        finally:
-            SCons.Node.FS.Mkdir = save_Mkdir
+        dir_made = []
+        new_dir = fs.Dir("new_dir")
+        new_dir.builder = Builder(fs.Dir, action=MkdirAction(dir_made))
+        xyz = fs.File(os.path.join("new_dir", "xyz"))
+
+        xyz.set_state(SCons.Node.up_to_date)
+        xyz.prepare()
+        assert dir_made == [], dir_made
+        xyz.set_state(0)
+        xyz.prepare()
+        print "dir_made[0] =", dir_made[0]
+        assert dir_made[0].path == "new_dir", dir_made[0]
 
         dir = fs.Dir("dir")
         dir.prepare()
index cd7aa18a9ce38a944cb797ba6211b2a03e533b22..72ddd74f390f9f7a77d91ef4bc83af1b58ef723c 100644 (file)
@@ -304,6 +304,14 @@ class NodeTestCase(unittest.TestCase):
         n1.builder_set(Builder())
         assert n1.has_builder() == 1
 
+    def test_multiple_side_effect_has_builder(self):
+        """Test the multiple_side_effect_has_builder() method
+        """
+        n1 = SCons.Node.Node()
+        assert n1.multiple_side_effect_has_builder() == 0
+        n1.builder_set(Builder())
+        assert n1.multiple_side_effect_has_builder() == 1
+
     def test_is_derived(self):
         """Test the is_derived() method
         """
index 51e66287a87d4f5910fb04622474c899b9b9a549..603762ec1fed6cfab6424a5f5ac0617c853471d3 100644 (file)
@@ -299,6 +299,8 @@ class Node:
             b = self.builder
         return not b is None
 
+    multiple_side_effect_has_builder = has_builder
+
     def is_derived(self):
         """
         Returns true iff this node is derived (i.e. built).
diff --git a/test/Builder-factories.py b/test/Builder-factories.py
new file mode 100644 (file)
index 0000000..6007d45
--- /dev/null
@@ -0,0 +1,72 @@
+#!/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__"
+
+"""
+Test the ability to specify the target_factory and source_factory
+of a Builder.
+"""
+
+import os.path
+
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+test.subdir('src')
+
+test.write('SConstruct', """
+import os
+import os.path
+def mkdir(env, source, target):
+    t = str(target[0])
+    os.makedirs(t)
+    open(os.path.join(t, 'marker'), 'w').write("MakeDirectory\\n")
+MakeDirectory = Builder(action=mkdir, target_factory=Dir)
+def collect(env, source, target):
+    out = open(str(target[0]), 'w')
+    dir = str(source[0])
+    for f in os.listdir(dir):
+        f = os.path.join(dir, f)
+        out.write(open(f, 'r').read())
+    out.close()
+Collect = Builder(action=collect, source_factory=Dir)
+env = Environment(BUILDERS = {'MakeDirectory':MakeDirectory,
+                              'Collect':Collect})
+env.MakeDirectory('foo', [])
+env.Collect('output', 'src')
+""")
+
+test.write(['src', 'file1'], "src/file1\n")
+test.write(['src', 'file2'], "src/file2\n")
+test.write(['src', 'file3'], "src/file3\n")
+
+test.run(arguments = '.')
+
+test.fail_test(not os.path.isdir(test.workpath('foo')))
+test.must_match(["foo", "marker"], "MakeDirectory\n")
+test.must_match("output", "src/file1\nsrc/file2\nsrc/file3\n")
+
+test.pass_test()