Fix using more than two targets or sources in a list.
[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(string.split(x)),
130                        string.split(act, '\n'))
131         if len(listCmds) == 1:
132             return listCmds[0]
133         else:
134             return ListAction(listCmds)
135     else:
136         return None
137
138 def Action(act, strfunction=_null, varlist=[]):
139     """A factory for action objects."""
140     if SCons.Util.is_List(act):
141         acts = map(lambda x, s=strfunction, v=varlist:
142                           _do_create_action(x, s, v),
143                    act)
144         acts = filter(lambda x: not x is None, acts)
145         if len(acts) == 1:
146             return acts[0]
147         else:
148             return ListAction(acts)
149     else:
150         return _do_create_action(act, strfunction=strfunction, varlist=varlist)
151
152 class ActionBase:
153     """Base class for actions that create output objects."""
154     def __cmp__(self, other):
155         return cmp(self.__dict__, other.__dict__)
156
157     def show(self, string):
158         if print_actions:
159             print string
160
161     def get_actions(self):
162         return [self]
163
164     def subst_dict(self, target, source, env):
165         """Create a dictionary for substitution of construction
166         variables.
167
168         This translates the following special arguments:
169
170             env    - the construction environment itself,
171                      the values of which (CC, CCFLAGS, etc.)
172                      are copied straight into the dictionary
173
174             target - the target (object or array of objects),
175                      used to generate the TARGET and TARGETS
176                      construction variables
177
178             source - the source (object or array of objects),
179                      used to generate the SOURCES and SOURCE
180                      construction variables
181         """
182
183         dict = {}
184
185         for k,v in env.items(): dict[k] = v
186
187         if not SCons.Util.is_List(target):
188             target = [target]
189
190         dict['TARGETS'] = SCons.Util.PathList(map(os.path.normpath, map(str, target)))
191         if dict['TARGETS']:
192             dict['TARGET'] = dict['TARGETS'][0]
193
194         def rstr(x):
195             try:
196                 return x.rstr()
197             except AttributeError:
198                 return str(x)
199         if not SCons.Util.is_List(source):
200             source = [source]
201         dict['SOURCES'] = SCons.Util.PathList(map(os.path.normpath, map(rstr, source)))
202         if dict['SOURCES']:
203             dict['SOURCE'] = dict['SOURCES'][0]
204
205         return dict
206
207     def __add__(self, other):
208         return _actionAppend(self, other)
209
210     def __radd__(self, other):
211         return _actionAppend(other, self)
212
213 def _string_from_cmd_list(cmd_list):
214     """Takes a list of command line arguments and returns a pretty
215     representation for printing."""
216     cl = []
217     for arg in map(str, cmd_list):
218         if ' ' in arg or '\t' in arg:
219             arg = '"' + arg + '"'
220         cl.append(arg)
221     return string.join(cl)
222
223 _rm = re.compile(r'\$[()]')
224 _remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
225
226 class CommandAction(ActionBase):
227     """Class for command-execution actions."""
228     def __init__(self, cmd):
229         self.cmd_list = cmd
230
231     def strfunction(self, target, source, env):
232         dict = self.subst_dict(target, source, env)
233         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, dict, {}, _rm)
234         return map(_string_from_cmd_list, cmd_list)
235
236     def __call__(self, target, source, env):
237         """Execute a command action.
238
239         This will handle lists of commands as well as individual commands,
240         because construction variable substitution may turn a single
241         "command" into a list.  This means that this class can actually
242         handle lists of commands, even though that's not how we use it
243         externally.
244         """
245         import SCons.Util
246
247         escape = env.get('ESCAPE', lambda x: x)
248
249         if env.has_key('SHELL'):
250             shell = env['SHELL']
251         else:
252             raise SCons.Errors.UserError('Missing SHELL construction variable.')
253
254         if env.has_key('SPAWN'):
255             spawn = env['SPAWN']
256         else:
257             raise SCons.Errors.UserError('Missing SPAWN construction variable.')
258
259         dict = self.subst_dict(target, source, env)
260         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, dict, {}, _rm)
261         for cmd_line in cmd_list:
262             if len(cmd_line):
263                 if print_actions:
264                     self.show(_string_from_cmd_list(cmd_line))
265                 if execute_actions:
266                     try:
267                         ENV = dict['ENV']
268                     except KeyError:
269                         global default_ENV
270                         if not default_ENV:
271                             import SCons.Environment
272                             default_ENV = SCons.Environment.Environment()['ENV']
273                         ENV = default_ENV
274                     # Escape the command line for the command
275                     # interpreter we are using
276                     map(lambda x, e=escape: x.escape(e), cmd_line)
277                     cmd_line = map(str, cmd_line)
278                     ret = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
279                     if ret:
280                         return ret
281         return 0
282
283     def _sig_dict(self, target, source, env):
284         """Supply a dictionary for use in computing signatures.
285
286         For signature purposes, it doesn't matter what targets or
287         sources we use, so long as we use the same ones every time
288         so the signature stays the same.  We supply an array of two
289         of each to allow for distinction between TARGET and TARGETS.
290         """
291         return self.subst_dict(['__t1__', '__t2__'], ['__s1__', '__s2__'], env)
292
293     def get_raw_contents(self, target, source, env):
294         """Return the complete contents of this action's command line.
295         """
296         # We've discusssed using the real target and source names in
297         # a CommandAction's signature contents.  This would have the
298         # advantage of recompiling when a file's name changes (keeping
299         # debug info current), but it would currently break repository
300         # logic that will change the file name based on whether the
301         # files come from a repository or locally.  If we ever move to
302         # that scheme, though, here's how we'd do it:
303         #return SCons.Util.scons_subst(string.join(self.cmd_list),
304         #                              self.subst_dict(target, source, env),
305         #                              {})
306         return SCons.Util.scons_subst(string.join(self.cmd_list),
307                                       env.sig_dict(),
308                                       {})
309
310     def get_contents(self, target, source, env):
311         """Return the signature contents of this action's command line.
312
313         This strips $(-$) and everything in between the string,
314         since those parts don't affect signatures.
315         """
316         # We've discusssed using the real target and source names in
317         # a CommandAction's signature contents.  This would have the
318         # advantage of recompiling when a file's name changes (keeping
319         # debug info current), but it would currently break repository
320         # logic that will change the file name based on whether the
321         # files come from a repository or locally.  If we ever move to
322         # that scheme, though, here's how we'd do it:
323         #return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)),
324         #                              self.subst_dict(target, source, env),
325         #                              {},
326         #                              _remove)
327         return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)),
328                                       env.sig_dict(),
329                                       {},
330                                       _remove)
331
332 class CommandGeneratorAction(ActionBase):
333     """Class for command-generator actions."""
334     def __init__(self, generator):
335         self.generator = generator
336
337     def __generate(self, target, source, env, for_signature):
338         # ensure that target is a list, to make it easier to write
339         # generator functions:
340         if not SCons.Util.is_List(target):
341             target = [target]
342
343         ret = self.generator(target=target, source=source, env=env, for_signature=for_signature)
344         gen_cmd = Action(ret)
345         if not gen_cmd:
346             raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
347         return gen_cmd
348
349     def __call__(self, target, source, env):
350         if not SCons.Util.is_List(source):
351             source = [source]
352         rsources = map(rfile, source)
353         act = self.__generate(target, source, env, 0)
354         return act(target, rsources, env)
355
356     def get_contents(self, target, source, env):
357         """Return the signature contents of this action's command line.
358
359         This strips $(-$) and everything in between the string,
360         since those parts don't affect signatures.
361         """
362         return self.__generate(target, source, env, 1).get_contents(target, source, env)
363
364 class LazyCmdGenerator:
365     """This is a simple callable class that acts as a command generator.
366     It holds on to a key into an Environment dictionary, then waits
367     until execution time to see what type it is, then tries to
368     create an Action out of it."""
369     def __init__(self, var):
370         self.var = SCons.Util.to_String(var)
371
372     def __call__(self, target, source, env, for_signature):
373         if env.has_key(self.var):
374             return env[self.var]
375         else:
376             # The variable reference substitutes to nothing.
377             return ''
378
379 class FunctionAction(ActionBase):
380     """Class for Python function actions."""
381
382     def __init__(self, execfunction, strfunction=_null, varlist=[]):
383         self.execfunction = execfunction
384         if strfunction is _null:
385             def strfunction(target, source, env, execfunction=execfunction):
386                 def quote(s):
387                     return '"' + str(s) + '"'
388                 def array(a, q=quote):
389                     return '[' + string.join(map(lambda x, q=q: q(x), a), ", ") + ']'
390                 try:
391                     name = execfunction.__name__
392                 except AttributeError:
393                     try:
394                         name = execfunction.__class__.__name__
395                     except AttributeError:
396                         name = "unknown_python_function"
397                 tstr = len(target) == 1 and quote(target[0]) or array(target)
398                 sstr = len(source) == 1 and quote(source[0]) or array(source)
399                 return "%s(%s, %s)" % (name, tstr, sstr)
400         self.strfunction = strfunction
401         self.varlist = varlist
402
403     def __call__(self, target, source, env):
404         r = 0
405         if not SCons.Util.is_List(target):
406             target = [target]
407         if not SCons.Util.is_List(source):
408             source = [source]
409         if print_actions and self.strfunction:
410             s = self.strfunction(target, source, env)
411             if s:
412                 self.show(s)
413         if execute_actions:
414             rsources = map(rfile, source)
415             r = self.execfunction(target=target, source=rsources, env=env)
416         return r
417
418     def get_contents(self, target, source, env):
419         """Return the signature contents of this callable action.
420
421         By providing direct access to the code object of the
422         function, Python makes this extremely easy.  Hooray!
423         """
424         try:
425             # "self.execfunction" is a function.
426             code = self.execfunction.func_code.co_code
427         except:
428             # "self.execfunction" is a callable object.
429             code = self.execfunction.__call__.im_func.func_code.co_code
430         return str(code) + string.join(map(lambda v, e=env: str(e[v]),
431                                        self.varlist))
432
433 class ListAction(ActionBase):
434     """Class for lists of other actions."""
435     def __init__(self, list):
436         self.list = map(lambda x: Action(x), list)
437
438     def get_actions(self):
439         return self.list
440
441     def __call__(self, target, source, env):
442         for l in self.list:
443             r = l(target, source, env)
444             if r:
445                 return r
446         return 0
447
448     def get_contents(self, target, source, env):
449         """Return the signature contents of this action list.
450
451         Simple concatenation of the signatures of the elements.
452         """
453         return string.join(map(lambda x, t=target, s=source, e=env:
454                                       x.get_contents(t, s, e),
455                                self.list),
456                            "")