Support using construction variables as re-usable, callable command generators. ...
[scons.git] / src / engine / SCons / Action.py
1 """engine.SCons.Action
2
3 XXX
4
5 """
6
7 #
8 # __COPYRIGHT__
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 import os
33 import os.path
34 import re
35 import string
36
37 import SCons.Errors
38 import SCons.Util
39
40 class _Null:
41     pass
42
43 _null = _Null
44
45 print_actions = 1;
46 execute_actions = 1;
47
48 exitvalmap = {
49     2 : 127,
50     13 : 126,
51 }
52
53 default_ENV = None
54
55 def rfile(n):
56     try:
57         return n.rfile()
58     except AttributeError:
59         return n
60
61 def SetCommandHandler(func, escape = lambda x: x):
62     raise SCons.Errors.UserError("SetCommandHandler() is no longer supported, use the SPAWN and ESCAPE construction variables.")
63
64 def GetCommandHandler():
65     raise SCons.Errors.UserError("GetCommandHandler() is no longer supported, use the SPAWN construction variable.")
66
67 def _actionAppend(act1, act2):
68     # This function knows how to slap two actions together.
69     # Mainly, it handles ListActions by concatenating into
70     # a single ListAction.
71     a1 = Action(act1)
72     a2 = Action(act2)
73     if a1 is None or a2 is None:
74         raise TypeError, "Cannot append %s to %s" % (type(act1), type(act2))
75     if isinstance(a1, ListAction):
76         if isinstance(a2, ListAction):
77             return ListAction(a1.list + a2.list)
78         else:
79             return ListAction(a1.list + [ a2 ])
80     else:
81         if isinstance(a2, ListAction):
82             return ListAction([ a1 ] + a2.list)
83         else:
84             return ListAction([ a1, a2 ])
85
86 class CommandGenerator:
87     """
88     Wraps a command generator function so the Action() factory
89     function can tell a generator function from a function action.
90     """
91     def __init__(self, generator):
92         self.generator = generator
93
94     def __add__(self, other):
95         return _actionAppend(self, other)
96
97     def __radd__(self, other):
98         return _actionAppend(other, self)
99
100 def _do_create_action(act, strfunction=_null, varlist=[]):
101     """This is the actual "implementation" for the
102     Action factory method, below.  This handles the
103     fact that passing lists to Action() itself has
104     different semantics than passing lists as elements
105     of lists.
106
107     The former will create a ListAction, the latter
108     will create a CommandAction by converting the inner
109     list elements to strings."""
110
111     if isinstance(act, ActionBase):
112         return act
113     elif SCons.Util.is_List(act):
114         return CommandAction(act)
115     elif isinstance(act, CommandGenerator):
116         return CommandGeneratorAction(act.generator)
117     elif callable(act):
118         return FunctionAction(act, strfunction=strfunction, varlist=varlist)
119     elif SCons.Util.is_String(act):
120         var=SCons.Util.get_environment_var(act)
121         if var:
122             # This looks like a string that is purely an Environment
123             # variable reference, like "$FOO" or "${FOO}".  We do
124             # something special here...we lazily evaluate the contents
125             # of that Environment variable, so a user could but something
126             # like a function or a CommandGenerator in that variable
127             # instead of a string.
128             return CommandGeneratorAction(LazyCmdGenerator(var))
129         listCmds = map(lambda x: CommandAction(x), string.split(act, '\n'))
130         if len(listCmds) == 1:
131             return listCmds[0]
132         else:
133             return ListAction(listCmds)
134     else:
135         return None
136
137 def Action(act, strfunction=_null, varlist=[]):
138     """A factory for action objects."""
139     if SCons.Util.is_List(act):
140         acts = map(lambda x, s=strfunction, v=varlist:
141                           _do_create_action(x, s, v),
142                    act)
143         acts = filter(lambda x: not x is None, acts)
144         if len(acts) == 1:
145             return acts[0]
146         else:
147             return ListAction(acts)
148     else:
149         return _do_create_action(act, strfunction=strfunction, varlist=varlist)
150
151 class ActionBase:
152     """Base class for actions that create output objects."""
153     def __cmp__(self, other):
154         return cmp(self.__dict__, other.__dict__)
155
156     def show(self, string):
157         if print_actions:
158             print string
159
160     def get_actions(self):
161         return [self]
162
163     def __add__(self, other):
164         return _actionAppend(self, other)
165
166     def __radd__(self, other):
167         return _actionAppend(other, self)
168
169 def _string_from_cmd_list(cmd_list):
170     """Takes a list of command line arguments and returns a pretty
171     representation for printing."""
172     cl = []
173     for arg in map(str, cmd_list):
174         if ' ' in arg or '\t' in arg:
175             arg = '"' + arg + '"'
176         cl.append(arg)
177     return string.join(cl)
178
179 _rm = re.compile(r'\$[()]')
180 _remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
181
182 class CommandAction(ActionBase):
183     """Class for command-execution actions."""
184     def __init__(self, cmd):
185         # Cmd list can actually be a list or a single item...basically
186         # anything that we could pass in as the first arg to
187         # scons_subst_list().
188         self.cmd_list = cmd
189
190     def strfunction(self, target, source, env):
191         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
192                                                target, source)
193         return map(_string_from_cmd_list, cmd_list)
194
195     def __call__(self, target, source, env):
196         """Execute a command action.
197
198         This will handle lists of commands as well as individual commands,
199         because construction variable substitution may turn a single
200         "command" into a list.  This means that this class can actually
201         handle lists of commands, even though that's not how we use it
202         externally.
203         """
204         import SCons.Util
205
206         escape = env.get('ESCAPE', lambda x: x)
207
208         if env.has_key('SHELL'):
209             shell = env['SHELL']
210         else:
211             raise SCons.Errors.UserError('Missing SHELL construction variable.')
212
213         if env.has_key('SPAWN'):
214             spawn = env['SPAWN']
215         else:
216             raise SCons.Errors.UserError('Missing SPAWN construction variable.')
217
218         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
219                                                target, source)
220         for cmd_line in cmd_list:
221             if len(cmd_line):
222                 if print_actions:
223                     self.show(_string_from_cmd_list(cmd_line))
224                 if execute_actions:
225                     try:
226                         ENV = env['ENV']
227                     except KeyError:
228                         global default_ENV
229                         if not default_ENV:
230                             import SCons.Environment
231                             default_ENV = SCons.Environment.Environment()['ENV']
232                         ENV = default_ENV
233                     # Escape the command line for the command
234                     # interpreter we are using
235                     map(lambda x, e=escape: x.escape(e), cmd_line)
236                     cmd_line = map(str, cmd_line)
237                     ret = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
238                     if ret:
239                         return ret
240         return 0
241
242     def get_raw_contents(self, target, source, env):
243         """Return the complete contents of this action's command line.
244         """
245         # We've discusssed using the real target and source names in
246         # a CommandAction's signature contents.  This would have the
247         # advantage of recompiling when a file's name changes (keeping
248         # debug info current), but it would currently break repository
249         # logic that will change the file name based on whether the
250         # files come from a repository or locally.  If we ever move to
251         # that scheme, though, here's how we'd do it:
252         #return SCons.Util.scons_subst(string.join(self.cmd_list),
253         #                              self.subst_dict(target, source, env),
254         #                              {})
255         cmd = self.cmd_list
256         if not SCons.Util.is_List(cmd):
257             cmd = [ cmd ]
258         return SCons.Util.scons_subst(string.join(map(str, cmd)),
259                                       env)
260
261     def get_contents(self, target, source, env):
262         """Return the signature contents of this action's command line.
263
264         This strips $(-$) and everything in between the string,
265         since those parts don't affect signatures.
266         """
267         # We've discusssed using the real target and source names in
268         # a CommandAction's signature contents.  This would have the
269         # advantage of recompiling when a file's name changes (keeping
270         # debug info current), but it would currently break repository
271         # logic that will change the file name based on whether the
272         # files come from a repository or locally.  If we ever move to
273         # that scheme, though, here's how we'd do it:
274         #return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)),
275         #                              self.subst_dict(target, source, env),
276         #                              {},
277         #                              _remove)
278         cmd = self.cmd_list
279         if not SCons.Util.is_List(cmd):
280             cmd = [ cmd ]
281         return SCons.Util.scons_subst(string.join(map(str, cmd)),
282                                       env,
283                                       _remove)
284
285 class CommandGeneratorAction(ActionBase):
286     """Class for command-generator actions."""
287     def __init__(self, generator):
288         self.generator = generator
289
290     def __generate(self, target, source, env, for_signature):
291         # ensure that target is a list, to make it easier to write
292         # generator functions:
293         if not SCons.Util.is_List(target):
294             target = [target]
295
296         ret = self.generator(target=target, source=source, env=env, for_signature=for_signature)
297         gen_cmd = Action(ret)
298         if not gen_cmd:
299             raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
300         return gen_cmd
301
302     def __call__(self, target, source, env):
303         if not SCons.Util.is_List(source):
304             source = [source]
305         rsources = map(rfile, source)
306         act = self.__generate(target, source, env, 0)
307         return act(target, rsources, env)
308
309     def get_contents(self, target, source, env):
310         """Return the signature contents of this action's command line.
311
312         This strips $(-$) and everything in between the string,
313         since those parts don't affect signatures.
314         """
315         return self.__generate(target, source, env, 1).get_contents(target, source, env)
316
317 class LazyCmdGenerator:
318     """This is a simple callable class that acts as a command generator.
319     It holds on to a key into an Environment dictionary, then waits
320     until execution time to see what type it is, then tries to
321     create an Action out of it."""
322     def __init__(self, var):
323         self.var = SCons.Util.to_String(var)
324
325     def __call__(self, target, source, env, for_signature):
326         if env.has_key(self.var):
327             return env[self.var]
328         else:
329             # The variable reference substitutes to nothing.
330             return ''
331
332 class FunctionAction(ActionBase):
333     """Class for Python function actions."""
334
335     def __init__(self, execfunction, strfunction=_null, varlist=[]):
336         self.execfunction = execfunction
337         if strfunction is _null:
338             def strfunction(target, source, env, execfunction=execfunction):
339                 def quote(s):
340                     return '"' + str(s) + '"'
341                 def array(a, q=quote):
342                     return '[' + string.join(map(lambda x, q=q: q(x), a), ", ") + ']'
343                 try:
344                     name = execfunction.__name__
345                 except AttributeError:
346                     try:
347                         name = execfunction.__class__.__name__
348                     except AttributeError:
349                         name = "unknown_python_function"
350                 tstr = len(target) == 1 and quote(target[0]) or array(target)
351                 sstr = len(source) == 1 and quote(source[0]) or array(source)
352                 return "%s(%s, %s)" % (name, tstr, sstr)
353         self.strfunction = strfunction
354         self.varlist = varlist
355
356     def __call__(self, target, source, env):
357         r = 0
358         if not SCons.Util.is_List(target):
359             target = [target]
360         if not SCons.Util.is_List(source):
361             source = [source]
362         if print_actions and self.strfunction:
363             s = self.strfunction(target, source, env)
364             if s:
365                 self.show(s)
366         if execute_actions:
367             rsources = map(rfile, source)
368             r = self.execfunction(target=target, source=rsources, env=env)
369         return r
370
371     def get_contents(self, target, source, env):
372         """Return the signature contents of this callable action.
373
374         By providing direct access to the code object of the
375         function, Python makes this extremely easy.  Hooray!
376         """
377         try:
378             # "self.execfunction" is a function.
379             code = self.execfunction.func_code.co_code
380         except:
381             # "self.execfunction" is a callable object.
382             code = self.execfunction.__call__.im_func.func_code.co_code
383         return str(code) + string.join(map(lambda v, e=env: str(e[v]),
384                                        self.varlist))
385
386 class ListAction(ActionBase):
387     """Class for lists of other actions."""
388     def __init__(self, list):
389         self.list = map(lambda x: Action(x), list)
390
391     def get_actions(self):
392         return self.list
393
394     def __call__(self, target, source, env):
395         for l in self.list:
396             r = l(target, source, env)
397             if r:
398                 return r
399         return 0
400
401     def get_contents(self, target, source, env):
402         """Return the signature contents of this action list.
403
404         Simple concatenation of the signatures of the elements.
405         """
406         return string.join(map(lambda x, t=target, s=source, e=env:
407                                       x.get_contents(t, s, e),
408                                self.list),
409                            "")