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