Several bug fixes from Charles Crain.
[scons.git] / src / engine / SCons / Action.py
1 """engine.SCons.Action
2
3 XXX
4
5 """
6
7 #
8 # Copyright (c) 2001, 2002 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 import copy
33 import os
34 import os.path
35 import re
36 import string
37 import sys
38 import UserDict
39
40 import SCons.Util
41 import SCons.Errors
42
43 print_actions = 1;
44 execute_actions = 1;
45
46 exitvalmap = {
47     2 : 127,
48     13 : 126,
49 }
50
51 if os.name == 'posix':
52
53     def defaultSpawn(cmd, args, env):
54         pid = os.fork()
55         if not pid:
56             # Child process.
57             exitval = 127
58             args = [ 'sh', '-c' ] + \
59                    [ string.join(map(lambda x: string.replace(str(x),
60                                                               ' ',
61                                                               r'\ '),
62                                      args)) ]
63             try:
64                 os.execvpe('sh', args, env)
65             except OSError, e:
66                 exitval = exitvalmap[e[0]]
67                 sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
68             os._exit(exitval)
69         else:
70             # Parent process.
71             pid, stat = os.waitpid(pid, 0)
72             ret = stat >> 8
73             return ret
74
75 elif os.name == 'nt':
76
77     def pathsearch(cmd, env):
78         # In order to deal with the fact that 1.5.2 doesn't have
79         # os.spawnvpe(), roll our own PATH search.
80         if os.path.isabs(cmd):
81             if not os.path.exists(cmd):
82                 exts = env['PATHEXT']
83                 if not SCons.Util.is_List(exts):
84                     exts = string.split(exts, os.pathsep)
85                 for e in exts:
86                     f = cmd + e
87                     if os.path.exists(f):
88                         return f
89             else:
90                 return cmd
91         else:
92             path = env['PATH']
93             if not SCons.Util.is_List(path):
94                 path = string.split(path, os.pathsep)
95             exts = env['PATHEXT']
96             if not SCons.Util.is_List(exts):
97                 exts = string.split(exts, os.pathsep)
98             pairs = []
99             for dir in path:
100                 for e in exts:
101                     pairs.append((dir, e))
102             for dir, ext in pairs:
103                 f = os.path.join(dir, cmd)
104                 if not ext is None:
105                     f = f + ext
106                 if os.path.exists(f):
107                     return f
108         return None
109
110     # Attempt to find cmd.exe (for WinNT/2k/XP) or
111     # command.com for Win9x
112
113     cmd_interp = ''
114     # First see if we can look in the registry...
115     if SCons.Util.can_read_reg:
116         try:
117             # Look for Windows NT system root
118             k=SCons.Util.RegOpenKeyEx(SCons.Util.hkey_mod.HKEY_LOCAL_MACHINE,
119                                           'Software\\Microsoft\\Windows NT\\CurrentVersion')
120             val, tok = SCons.Util.RegQueryValueEx(k, 'SystemRoot')
121             cmd_interp = os.path.join(val, 'System32\\cmd.exe')
122         except SCons.Util.RegError:
123             try:
124                 # Okay, try the Windows 9x system root
125                 k=SCons.Util.RegOpenKeyEx(SCons.Util.hkey_mod.HKEY_LOCAL_MACHINE,
126                                               'Software\\Microsoft\\Windows\\CurrentVersion')
127                 val, tok = SCons.Util.RegQueryValueEx(k, 'SystemRoot')
128                 cmd_interp = os.path.join(val, 'command.com')
129             except:
130                 pass
131     if not cmd_interp:
132         cmd_interp = pathsearch('cmd', os.environ)
133         if not cmd_interp:
134             cmd_interp = pathsearch('command', os.environ)
135
136     # The upshot of all this is that, if you are using Python 1.5.2,
137     # you had better have cmd or command.com in your PATH when you run
138     # scons.
139
140     def defaultSpawn(cmd, args, env):
141         if not cmd_interp:
142             sys.stderr.write("scons: Could not find command interpreter, is it in your PATH?\n")
143             return 127
144         else:
145             try:
146
147                 a = [ cmd_interp, '/C', args[0] ]
148                 for arg in args[1:]:
149                     if ' ' in arg or '\t' in arg:
150                         arg = '"' + arg + '"'
151                     a.append(arg)
152                 ret = os.spawnve(os.P_WAIT, cmd_interp, a, env)
153             except OSError, e:
154                 ret = exitvalmap[e[0]]
155                 sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
156             return ret
157 else:
158     def defaultSpawn(cmd, args, env):
159         sys.stderr.write("scons: Unknown os '%s', cannot spawn command interpreter.\n" % os.name)
160         sys.stderr.write("scons: Set your command handler with SetCommandHandler().\n")
161         return 127
162
163 spawn = defaultSpawn
164
165 def SetCommandHandler(func):
166     global spawn
167     spawn = func
168
169 def GetCommandHandler():
170     global spawn
171     return spawn
172
173 class CommandGenerator:
174     """
175     Wraps a command generator function so the Action() factory
176     function can tell a generator function from a function action.
177     """
178     def __init__(self, generator):
179         self.generator = generator
180
181 def _do_create_action(act):
182     """This is the actual "implementation" for the
183     Action factory method, below.  This handles the
184     fact that passing lists to Action() itself has
185     different semantics than passing lists as elements
186     of lists.
187
188     The former will create a ListAction, the latter
189     will create a CommandAction by converting the inner
190     list elements to strings."""
191
192     if isinstance(act, ActionBase):
193         return act
194     elif SCons.Util.is_List(act):
195         return CommandAction(act)
196     elif isinstance(act, CommandGenerator):
197         return CommandGeneratorAction(act.generator)
198     elif callable(act):
199         return FunctionAction(act)
200     elif SCons.Util.is_String(act):
201         var=SCons.Util.get_environment_var(act)
202         if var:
203             # This looks like a string that is purely an Environment
204             # variable reference, like "$FOO" or "${FOO}".  We do
205             # something special here...we lazily evaluate the contents
206             # of that Environment variable, so a user could but something
207             # like a function or a CommandGenerator in that variable
208             # instead of a string.
209             return CommandGeneratorAction(LazyCmdGenerator(var))
210         listCmds = map(lambda x: CommandAction(string.split(x)),
211                        string.split(act, '\n'))
212         if len(listCmds) == 1:
213             return listCmds[0]
214         else:
215             return ListAction(listCmds)
216     else:
217         return None
218
219 def Action(act):
220     """A factory for action objects."""
221     if SCons.Util.is_List(act):
222         acts = filter(lambda x: not x is None,
223                       map(_do_create_action, act))
224         if len(acts) == 1:
225             return acts[0]
226         else:
227             return ListAction(acts)
228     else:
229         return _do_create_action(act)
230
231 class ActionBase:
232     """Base class for actions that create output objects."""
233     def __cmp__(self, other):
234         return cmp(self.__dict__, other.__dict__)
235
236     def show(self, string):
237         print string
238
239     def subst_dict(self, **kw):
240         """Create a dictionary for substitution of construction
241         variables.
242
243         This translates the following special arguments:
244
245             env    - the construction environment itself,
246                      the values of which (CC, CCFLAGS, etc.)
247                      are copied straight into the dictionary
248
249             target - the target (object or array of objects),
250                      used to generate the TARGET and TARGETS
251                      construction variables
252
253             source - the source (object or array of objects),
254                      used to generate the SOURCES and SOURCE 
255                      construction variables
256
257         Any other keyword arguments are copied into the
258         dictionary."""
259
260         dict = {}
261         if kw.has_key('env'):
262             dict.update(kw['env'])
263             del kw['env']
264
265         try:
266             cwd = kw['dir']
267         except:
268             cwd = None
269         else:
270             del kw['dir']
271
272         if kw.has_key('target'):
273             t = kw['target']
274             del kw['target']
275             if not SCons.Util.is_List(t):
276                 t = [t]
277             try:
278                 cwd = t[0].cwd
279             except (IndexError, AttributeError):
280                 pass
281             dict['TARGETS'] = SCons.Util.PathList(map(os.path.normpath, map(str, t)))
282             if dict['TARGETS']:
283                 dict['TARGET'] = dict['TARGETS'][0]
284
285         if kw.has_key('source'):
286             s = kw['source']
287             del kw['source']
288             if not SCons.Util.is_List(s):
289                 s = [s]
290             dict['SOURCES'] = SCons.Util.PathList(map(os.path.normpath, map(str, s)))
291             if dict['SOURCES']:
292                 dict['SOURCE'] = dict['SOURCES'][0]
293
294         dict.update(kw)
295
296         return dict
297
298 def _string_from_cmd_list(cmd_list):
299     """Takes a list of command line arguments and returns a pretty
300     representation for printing."""
301     cl = []
302     for arg in cmd_list:
303         if ' ' in arg or '\t' in arg:
304             arg = '"' + arg + '"'
305         cl.append(arg)
306     return string.join(cl)
307
308 _rm = re.compile(r'\$[()]')
309 _remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
310
311 class EnvDictProxy(UserDict.UserDict):
312     """This is a dictionary-like class that contains the
313     Environment dictionary we pass to FunctionActions
314     and CommandGeneratorActions.
315
316     In addition to providing
317     normal dictionary-like access to the variables in the
318     Environment, it also exposes the functions subst()
319     and subst_list(), allowing users to easily do variable
320     interpolation when writing their FunctionActions
321     and CommandGeneratorActions."""
322
323     def __init__(self, env):
324         UserDict.UserDict.__init__(self, env)
325
326     def subst(self, string, raw=0):
327         if raw:
328             regex_remove = None
329         else:
330             regex_remove = _rm
331         return SCons.Util.scons_subst(string, self.data, {}, regex_remove)
332
333     def subst_list(self, string, raw=0):
334         if raw:
335             regex_remove = None
336         else:
337             regex_remove = _rm
338         return SCons.Util.scons_subst_list(string, self.data, {}, regex_remove)
339
340 class CommandAction(ActionBase):
341     """Class for command-execution actions."""
342     def __init__(self, cmd):
343         import SCons.Util
344         
345         self.cmd_list = map(SCons.Util.to_String, cmd)
346
347     def execute(self, **kw):
348         dict = apply(self.subst_dict, (), kw)
349         import SCons.Util
350         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, dict, {}, _rm)
351         for cmd_line in cmd_list:
352             if len(cmd_line):
353                 if print_actions:
354                     self.show(_string_from_cmd_list(cmd_line))
355                 if execute_actions:
356                     try:
357                         ENV = kw['env']['ENV']
358                     except:
359                         import SCons.Defaults
360                         ENV = SCons.Defaults.ConstructionEnvironment['ENV']
361                     ret = spawn(cmd_line[0], cmd_line, ENV)
362                     if ret:
363                         return ret
364         return 0
365
366     def _sig_dict(self, kw):
367         """Supply a dictionary for use in computing signatures.
368
369         For signature purposes, it doesn't matter what targets or
370         sources we use, so long as we use the same ones every time
371         so the signature stays the same.  We supply an array of two
372         of each to allow for distinction between TARGET and TARGETS.
373         """
374         kw['target'] = ['__t1__', '__t2__']
375         kw['source'] = ['__s1__', '__s2__']
376         return apply(self.subst_dict, (), kw)
377
378     def get_raw_contents(self, **kw):
379         """Return the complete contents of this action's command line.
380         """
381         return SCons.Util.scons_subst(string.join(self.cmd_list),
382                                       self._sig_dict(kw), {})
383
384     def get_contents(self, **kw):
385         """Return the signature contents of this action's command line.
386
387         This strips $(-$) and everything in between the string,
388         since those parts don't affect signatures.
389         """
390         return SCons.Util.scons_subst(string.join(self.cmd_list),
391                                       self._sig_dict(kw), {}, _remove)
392
393 class CommandGeneratorAction(ActionBase):
394     """Class for command-generator actions."""
395     def __init__(self, generator):
396         self.generator = generator
397
398     def __generate(self, kw):
399         import SCons.Util
400
401         # Wrap the environment dictionary in an EnvDictProxy
402         # object to make variable interpolation easier for the
403         # client.
404         args = copy.copy(kw)
405         if args.has_key("env") and not isinstance(args["env"], EnvDictProxy):
406             args["env"] = EnvDictProxy(args["env"])
407
408         # ensure that target is a list, to make it easier to write
409         # generator functions:
410         if args.has_key("target") and not SCons.Util.is_List(args["target"]):
411             args["target"] = [args["target"]]
412
413         ret = apply(self.generator, (), args)
414         gen_cmd = Action(ret)
415         if not gen_cmd:
416             raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
417         return gen_cmd
418
419     def execute(self, **kw):
420         return apply(self.__generate(kw).execute, (), kw)
421
422     def get_contents(self, **kw):
423         """Return the signature contents of this action's command line.
424
425         This strips $(-$) and everything in between the string,
426         since those parts don't affect signatures.
427         """
428         return apply(self.__generate(kw).get_contents, (), kw)
429
430 class LazyCmdGenerator:
431     """This is a simple callable class that acts as a command generator.
432     It holds on to a key into an Environment dictionary, then waits
433     until execution time to see what type it is, then tries to
434     create an Action out of it."""
435     def __init__(self, var):
436         self.var = SCons.Util.to_String(var)
437
438     def __call__(self, env, **kw):
439         if env.has_key(self.var):
440             return env[self.var]
441         else:
442             # The variable reference substitutes to nothing.
443             return ''
444
445 class FunctionAction(ActionBase):
446     """Class for Python function actions."""
447     def __init__(self, function):
448         self.function = function
449
450     def execute(self, **kw):
451         # if print_actions:
452         # XXX:  WHAT SHOULD WE PRINT HERE?
453         if execute_actions:
454             if kw.has_key('target') and not \
455                SCons.Util.is_List(kw['target']):
456                 kw['target'] = [ kw['target'] ]
457             if kw.has_key('source') and not \
458                SCons.Util.is_List(kw['source']):
459                 kw['source'] = [ kw['source'] ]
460             if kw.has_key("env") and not isinstance(kw["env"], EnvDictProxy):
461                 kw["env"] = EnvDictProxy(kw["env"])
462             return apply(self.function, (), kw)
463
464     def get_contents(self, **kw):
465         """Return the signature contents of this callable action.
466
467         By providing direct access to the code object of the
468         function, Python makes this extremely easy.  Hooray!
469         """
470         #XXX DOES NOT ACCOUNT FOR CHANGES IN ENVIRONMENT VARIABLES
471         #THE FUNCTION MAY USE
472         try:
473             # "self.function" is a function.
474             code = self.function.func_code.co_code
475         except:
476             # "self.function" is a callable object.
477             code = self.function.__call__.im_func.func_code.co_code
478         return str(code)
479
480 class ListAction(ActionBase):
481     """Class for lists of other actions."""
482     def __init__(self, list):
483         self.list = map(lambda x: Action(x), list)
484
485     def execute(self, **kw):
486         for l in self.list:
487             r = apply(l.execute, (), kw)
488             if r != 0:
489                 return r
490         return 0
491
492     def get_contents(self, **kw):
493         """Return the signature contents of this action list.
494
495         Simple concatenation of the signatures of the elements.
496         """
497
498         return reduce(lambda x, y: x + str(y.get_contents()), self.list, "")