Evaluate Action varlists when calculating signatures. (Steve Christensen)
[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         # for SConf support (by now): check, if we want to pipe the command
214         # output to somewhere else
215         if env.has_key('PIPE_BUILD'):
216             pipe_build = 1
217             if env.has_key('PSPAWN'):
218                 pspawn = env['PSPAWN']
219             else:
220                 raise SCons.Errors.UserError('Missing PSPAWN construction variable.')
221             if env.has_key('PSTDOUT'):
222                 pstdout = env['PSTDOUT']
223             else:
224                 raise SCons.Errors.UserError('Missing PSTDOUT construction variable.')
225             if env.has_key('PSTDERR'):
226                 pstderr = env['PSTDERR']
227             else:
228                 raise SCons.Errors.UserError('Missing PSTDOUT construction variable.')
229         else:
230             pipe_build = 0
231             if env.has_key('SPAWN'):
232                 spawn = env['SPAWN']
233             else:
234                 raise SCons.Errors.UserError('Missing SPAWN construction variable.')
235
236         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
237                                                target, source)
238         for cmd_line in cmd_list:
239             if len(cmd_line):
240                 if print_actions:
241                     self.show(_string_from_cmd_list(cmd_line))
242                 if execute_actions:
243                     try:
244                         ENV = env['ENV']
245                     except KeyError:
246                         global default_ENV
247                         if not default_ENV:
248                             import SCons.Environment
249                             default_ENV = SCons.Environment.Environment()['ENV']
250                         ENV = default_ENV
251                     # Escape the command line for the command
252                     # interpreter we are using
253                     map(lambda x, e=escape: x.escape(e), cmd_line)
254                     cmd_line = map(str, cmd_line)
255                     if pipe_build:
256                         ret = pspawn( shell, escape, cmd_line[0], cmd_line,
257                                       ENV, pstdout, pstderr )
258                     else:
259                         ret = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
260                     if ret:
261                         return ret
262         return 0
263
264     def get_raw_contents(self, target, source, env):
265         """Return the complete contents of this action's command line.
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(self.cmd_list),
275         #                              self.subst_dict(target, source, env),
276         #                              {})
277         cmd = self.cmd_list
278         if not SCons.Util.is_List(cmd):
279             cmd = [ cmd ]
280         return SCons.Util.scons_subst(string.join(map(str, cmd)),
281                                       env)
282
283     def get_contents(self, target, source, env):
284         """Return the signature contents of this action's command line.
285
286         This strips $(-$) and everything in between the string,
287         since those parts don't affect signatures.
288         """
289         # We've discusssed using the real target and source names in
290         # a CommandAction's signature contents.  This would have the
291         # advantage of recompiling when a file's name changes (keeping
292         # debug info current), but it would currently break repository
293         # logic that will change the file name based on whether the
294         # files come from a repository or locally.  If we ever move to
295         # that scheme, though, here's how we'd do it:
296         #return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)),
297         #                              self.subst_dict(target, source, env),
298         #                              {},
299         #                              _remove)
300         cmd = self.cmd_list
301         if not SCons.Util.is_List(cmd):
302             cmd = [ cmd ]
303         return SCons.Util.scons_subst(string.join(map(str, cmd)),
304                                       env,
305                                       _remove)
306
307 class CommandGeneratorAction(ActionBase):
308     """Class for command-generator actions."""
309     def __init__(self, generator):
310         self.generator = generator
311
312     def __generate(self, target, source, env, for_signature):
313         # ensure that target is a list, to make it easier to write
314         # generator functions:
315         if not SCons.Util.is_List(target):
316             target = [target]
317
318         ret = self.generator(target=target, source=source, env=env, for_signature=for_signature)
319         gen_cmd = Action(ret)
320         if not gen_cmd:
321             raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
322         return gen_cmd
323
324     def __call__(self, target, source, env):
325         if not SCons.Util.is_List(source):
326             source = [source]
327         rsources = map(rfile, source)
328         act = self.__generate(target, source, env, 0)
329         return act(target, rsources, env)
330
331     def get_contents(self, target, source, env):
332         """Return the signature contents of this action's command line.
333
334         This strips $(-$) and everything in between the string,
335         since those parts don't affect signatures.
336         """
337         return self.__generate(target, source, env, 1).get_contents(target, source, env)
338
339 class LazyCmdGenerator:
340     """This is a simple callable class that acts as a command generator.
341     It holds on to a key into an Environment dictionary, then waits
342     until execution time to see what type it is, then tries to
343     create an Action out of it."""
344     def __init__(self, var):
345         self.var = SCons.Util.to_String(var)
346
347     def __call__(self, target, source, env, for_signature):
348         if env.has_key(self.var):
349             return env[self.var]
350         else:
351             # The variable reference substitutes to nothing.
352             return ''
353
354 class FunctionAction(ActionBase):
355     """Class for Python function actions."""
356
357     def __init__(self, execfunction, strfunction=_null, varlist=[]):
358         self.execfunction = execfunction
359         if strfunction is _null:
360             def strfunction(target, source, env, execfunction=execfunction):
361                 def quote(s):
362                     return '"' + str(s) + '"'
363                 def array(a, q=quote):
364                     return '[' + string.join(map(lambda x, q=q: q(x), a), ", ") + ']'
365                 try:
366                     name = execfunction.__name__
367                 except AttributeError:
368                     try:
369                         name = execfunction.__class__.__name__
370                     except AttributeError:
371                         name = "unknown_python_function"
372                 tstr = len(target) == 1 and quote(target[0]) or array(target)
373                 sstr = len(source) == 1 and quote(source[0]) or array(source)
374                 return "%s(%s, %s)" % (name, tstr, sstr)
375         self.strfunction = strfunction
376         self.varlist = varlist
377
378     def __call__(self, target, source, env):
379         r = 0
380         if not SCons.Util.is_List(target):
381             target = [target]
382         if not SCons.Util.is_List(source):
383             source = [source]
384         if print_actions and self.strfunction:
385             s = self.strfunction(target, source, env)
386             if s:
387                 self.show(s)
388         if execute_actions:
389             rsources = map(rfile, source)
390             r = self.execfunction(target=target, source=rsources, env=env)
391         return r
392
393     def get_contents(self, target, source, env):
394         """Return the signature contents of this callable action.
395
396         By providing direct access to the code object of the
397         function, Python makes this extremely easy.  Hooray!
398         """
399         try:
400             # "self.execfunction" is a function.
401             code = self.execfunction.func_code.co_code
402         except:
403             # "self.execfunction" is a callable object.
404             code = self.execfunction.__call__.im_func.func_code.co_code
405         return str(code) + env.subst(string.join(map(lambda v: '${'+v+'}',
406                                                      self.varlist)))
407
408 class ListAction(ActionBase):
409     """Class for lists of other actions."""
410     def __init__(self, list):
411         self.list = map(lambda x: Action(x), list)
412
413     def get_actions(self):
414         return self.list
415
416     def __call__(self, target, source, env):
417         for l in self.list:
418             r = l(target, source, env)
419             if r:
420                 return r
421         return 0
422
423     def get_contents(self, target, source, env):
424         """Return the signature contents of this action list.
425
426         Simple concatenation of the signatures of the elements.
427         """
428         return string.join(map(lambda x, t=target, s=source, e=env:
429                                       x.get_contents(t, s, e),
430                                self.list),
431                            "")