Fix bug 494991
[scons.git] / src / engine / SCons / Builder.py
1 """SCons.Builder
2
3 XXX
4
5 """
6
7 #
8 # Copyright (c) 2001 Steven Knight
9 #
10 # Permission is hereby granted, free of charge, to any person obtaining
11 # a copy of this software and associated documentation files (the
12 # "Software"), to deal in the Software without restriction, including
13 # without limitation the rights to use, copy, modify, merge, publish,
14 # distribute, sublicense, and/or sell copies of the Software, and to
15 # permit persons to whom the Software is furnished to do so, subject to
16 # the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be included
19 # in all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 #
29
30 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
31
32
33
34 import os.path
35 import string
36 from Errors import UserError
37
38 import SCons.Action
39 import SCons.Node.FS
40 import SCons.Util
41
42
43
44 def Builder(**kw):
45     """A factory for builder objects."""
46     if kw.has_key('action') and SCons.Util.is_Dict(kw['action']):
47         return apply(CompositeBuilder, (), kw)
48     elif kw.has_key('src_builder'):
49         return apply(MultiStepBuilder, (), kw)
50     else:
51         return apply(BuilderBase, (), kw)
52
53
54
55 class BuilderBase:
56     """Base class for Builders, objects that create output
57     nodes (files) from input nodes (files).
58     """
59
60     def __init__(self,  name = None,
61                         action = None,
62                         prefix = '',
63                         suffix = '',
64                         src_suffix = '',
65                         node_factory = SCons.Node.FS.default_fs.File,
66                         scanner = None):
67         if name is None:
68             raise UserError, "You must specify a name for the builder."
69         self.name = name
70         self.action = SCons.Action.Action(action)
71
72         self.prefix = prefix
73         self.suffix = suffix
74         self.src_suffix = src_suffix
75         self.node_factory = node_factory
76         self.scanner = scanner
77         if self.suffix and self.suffix[0] not in '.$':
78             self.suffix = '.' + self.suffix
79         if self.src_suffix and self.src_suffix[0] not in '.$':
80             self.src_suffix = '.' + self.src_suffix
81
82     def __cmp__(self, other):
83         return cmp(self.__dict__, other.__dict__)
84
85     def _create_nodes(self, env, target = None, source = None):
86         """Create and return lists of target and source nodes.
87         """
88         def adjustixes(files, pre, suf):
89             ret = []
90             if SCons.Util.is_String(files):
91                 files = string.split(files)
92             if not SCons.Util.is_List(files):
93                 files = [files]
94             for f in files:
95                 if SCons.Util.is_String(f):
96                     if pre and f[:len(pre)] != pre:
97                         path, fn = os.path.split(os.path.normpath(f))
98                         f = os.path.join(path, pre + fn)
99                     if suf:
100                         if f[-len(suf):] != suf:
101                             f = f + suf
102                 ret.append(f)
103             return ret
104
105         tlist = SCons.Util.scons_str2nodes(adjustixes(target,
106                                                       env.subst(self.prefix),
107                                                       env.subst(self.suffix)),
108                                            self.node_factory)
109
110         slist = SCons.Util.scons_str2nodes(adjustixes(source,
111                                                       None,
112                                                       env.subst(self.src_suffix)),
113                                            self.node_factory)
114         return tlist, slist
115
116     def _init_nodes(self, env, tlist, slist):
117         """Initialize lists of target and source nodes with all of
118         the proper Builder information.
119         """
120         for t in tlist:
121             t.cwd = SCons.Node.FS.default_fs.getcwd()   # XXX
122             t.builder_set(self)
123             t.env_set(env)
124             t.add_source(slist)
125             if self.scanner:
126                 t.scanner_set(self.scanner.instance(env))
127
128         for s in slist:
129             s.env_set(env, 1)
130             scanner = env.get_scanner(os.path.splitext(s.name)[1])
131             if scanner:
132                 s.scanner_set(scanner.instance(env))
133
134         if len(tlist) == 1:
135             tlist = tlist[0]
136         return tlist
137
138     def __call__(self, env, target = None, source = None):
139         tlist, slist = self._create_nodes(env, target, source)
140
141         return self._init_nodes(env, tlist, slist)
142
143     def execute(self, **kw):
144         """Execute a builder's action to create an output object.
145         """
146         return apply(self.action.execute, (), kw)
147
148     def get_contents(self, **kw):
149         """Fetch the "contents" of the builder's action
150         (for signature calculation).
151         """
152         return apply(self.action.get_contents, (), kw)
153
154     def src_suffixes(self):
155         if self.src_suffix != '':
156             return [self.src_suffix]
157         return []
158
159 class MultiStepBuilder(BuilderBase):
160     """This is a builder subclass that can build targets in
161     multiple steps.  The src_builder parameter to the constructor
162     accepts a builder that is called to build sources supplied to
163     this builder.  The targets of that first build then become
164     the sources of this builder.
165
166     If this builder has a src_suffix supplied, then the src_builder
167     builder is NOT invoked if the suffix of a source file matches
168     src_suffix.
169     """
170     def __init__(self,  src_builder,
171                         name = None,
172                         action = None,
173                         prefix = '',
174                         suffix = '',
175                         src_suffix = '',
176                         node_factory = SCons.Node.FS.default_fs.File,
177                         scanner=None):
178         BuilderBase.__init__(self, name, action, prefix, suffix, src_suffix,
179                              node_factory, scanner)
180         self.src_builder = src_builder
181
182     def __call__(self, env, target = None, source = None):
183         slist = SCons.Util.scons_str2nodes(source, self.node_factory)
184         final_sources = []
185         src_suffix = env.subst(self.src_suffix)
186         for snode in slist:
187             path, ext = os.path.splitext(snode.abspath)
188             if not src_suffix or ext != src_suffix:
189                 tgt = self.src_builder(env, target = [ path ],
190                                      source=snode)
191                 if not SCons.Util.is_List(tgt):
192                     final_sources.append(tgt)
193                 else:
194                     final_sources.extend(tgt)
195             else:
196                 final_sources.append(snode)
197         return BuilderBase.__call__(self, env, target=target,
198                                     source=final_sources)
199
200     def src_suffixes(self):
201         return BuilderBase.src_suffixes(self) + self.src_builder.src_suffixes()
202
203 class CompositeBuilder(BuilderBase):
204     """This is a convenient Builder subclass that can build different
205     files based on their suffixes.  For each target, this builder
206     will examine the target's sources.  If they are all the same
207     suffix, and that suffix is equal to one of the child builders'
208     src_suffix, then that child builder will be used.  Otherwise,
209     UserError is thrown."""
210     def __init__(self,  name = None,
211                         prefix='',
212                         suffix='',
213                         action = {},
214                         src_builder = []):
215         BuilderBase.__init__(self, name=name, prefix=prefix,
216                              suffix=suffix)
217         if src_builder and not SCons.Util.is_List(src_builder):
218             src_builder = [src_builder]
219         self.src_builder = src_builder
220         self.builder_dict = {}
221         for suff, act in action.items():
222              # Create subsidiary builders for every suffix in the
223              # action dictionary.  If there's a src_builder that
224              # matches the suffix, add that to the initializing
225              # keywords so that a MultiStepBuilder will get created.
226              kw = {'name' : name, 'action' : act, 'src_suffix' : suff}
227              src_bld = filter(lambda x, s=suff: x.suffix == s, self.src_builder)
228              if src_bld:
229                  kw['src_builder'] = src_bld[0]
230              self.builder_dict[suff] = apply(Builder, (), kw)
231
232     def __call__(self, env, target = None, source = None):
233         tlist, slist = BuilderBase._create_nodes(self, env,
234                                                  target=target, source=source)
235
236         # XXX These [bs]dict tables are invariant for each unique
237         # CompositeBuilder + Environment pair, so we should cache them.
238         bdict = {}
239         sdict = {}
240         for suffix, bld in self.builder_dict.items():
241             bdict[env.subst(bld.src_suffix)] = bld
242             sdict[suffix] = suffix
243             for s in bld.src_suffixes():
244                 bdict[s] = bld
245                 sdict[s] = suffix
246
247         for tnode in tlist:
248             suflist = map(lambda x, s=sdict: s[os.path.splitext(x.path)[1]],
249                           slist)
250             last_suffix=''
251             for suffix in suflist:
252                 if last_suffix and last_suffix != suffix:
253                     raise UserError, "The builder for %s can only build source files of identical suffixes:  %s." % (tnode.path, str(map(lambda t: str(t.path), tnode.sources)))
254                 last_suffix = suffix
255             if last_suffix:
256                 try:
257                     bdict[last_suffix].__call__(env, target = tnode,
258                                                 source = slist)
259                 except KeyError:
260                     raise UserError, "The builder for %s can not build files with suffix: %s" % (tnode.path, suffix)
261
262         if len(tlist) == 1:
263             tlist = tlist[0]
264         return tlist
265
266     def src_suffixes(self):
267         return reduce(lambda x, y: x + y,
268                       map(lambda b: b.src_suffixes(),
269                           self.builder_dict.values()))