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