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