Big change for shared libraries and a bunch of other flexibility. (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         listCmds = map(lambda x: CommandAction(string.split(x)),
202                        string.split(act, '\n'))
203         if len(listCmds) == 1:
204             return listCmds[0]
205         else:
206             return ListAction(listCmds)
207     else:
208         return None
209
210 def Action(act):
211     """A factory for action objects."""
212     if SCons.Util.is_List(act):
213         acts = filter(lambda x: not x is None,
214                       map(_do_create_action, act))
215         if len(acts) == 1:
216             return acts[0]
217         else:
218             return ListAction(acts)
219     else:
220         return _do_create_action(act)
221
222 class ActionBase:
223     """Base class for actions that create output objects."""
224     def __cmp__(self, other):
225         return cmp(self.__dict__, other.__dict__)
226
227     def show(self, string):
228         print string
229
230     def subst_dict(self, **kw):
231         """Create a dictionary for substitution of construction
232         variables.
233
234         This translates the following special arguments:
235
236             env    - the construction environment itself,
237                      the values of which (CC, CCFLAGS, etc.)
238                      are copied straight into the dictionary
239
240             target - the target (object or array of objects),
241                      used to generate the TARGET and TARGETS
242                      construction variables
243
244             source - the source (object or array of objects),
245                      used to generate the SOURCES and SOURCE 
246                      construction variables
247
248         Any other keyword arguments are copied into the
249         dictionary."""
250
251         dict = {}
252         if kw.has_key('env'):
253             dict.update(kw['env'])
254             del kw['env']
255
256         try:
257             cwd = kw['dir']
258         except:
259             cwd = None
260         else:
261             del kw['dir']
262
263         if kw.has_key('target'):
264             t = kw['target']
265             del kw['target']
266             if not SCons.Util.is_List(t):
267                 t = [t]
268             try:
269                 cwd = t[0].cwd
270             except (IndexError, AttributeError):
271                 pass
272             dict['TARGETS'] = SCons.Util.PathList(map(os.path.normpath, map(str, t)))
273             if dict['TARGETS']:
274                 dict['TARGET'] = dict['TARGETS'][0]
275
276         if kw.has_key('source'):
277             s = kw['source']
278             del kw['source']
279             if not SCons.Util.is_List(s):
280                 s = [s]
281             dict['SOURCES'] = SCons.Util.PathList(map(os.path.normpath, map(str, s)))
282             if dict['SOURCES']:
283                 dict['SOURCE'] = dict['SOURCES'][0]
284
285         dict.update(kw)
286
287         return dict
288
289 def _string_from_cmd_list(cmd_list):
290     """Takes a list of command line arguments and returns a pretty
291     representation for printing."""
292     cl = []
293     for arg in cmd_list:
294         if ' ' in arg or '\t' in arg:
295             arg = '"' + arg + '"'
296         cl.append(arg)
297     return string.join(cl)
298
299 _rm = re.compile(r'\$[()]')
300 _remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
301
302 class EnvDictProxy(UserDict.UserDict):
303     """This is a dictionary-like class that contains the
304     Environment dictionary we pass to FunctionActions
305     and CommandGeneratorActions.
306
307     In addition to providing
308     normal dictionary-like access to the variables in the
309     Environment, it also exposes the functions subst()
310     and subst_list(), allowing users to easily do variable
311     interpolation when writing their FunctionActions
312     and CommandGeneratorActions."""
313
314     def __init__(self, env):
315         UserDict.UserDict.__init__(self, env)
316
317     def subst(self, string):
318         return SCons.Util.scons_subst(string, self.data, {}, _rm)
319
320     def subst_list(self, string):
321         return SCons.Util.scons_subst_list(string, self.data, {}, _rm)
322
323 class CommandAction(ActionBase):
324     """Class for command-execution actions."""
325     def __init__(self, cmd):
326         import SCons.Util
327         
328         self.cmd_list = map(SCons.Util.to_String, cmd)
329
330     def execute(self, **kw):
331         dict = apply(self.subst_dict, (), kw)
332         import SCons.Util
333         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, dict, {}, _rm)
334         for cmd_line in cmd_list:
335             if len(cmd_line):
336                 if print_actions:
337                     self.show(_string_from_cmd_list(cmd_line))
338                 if execute_actions:
339                     try:
340                         ENV = kw['env']['ENV']
341                     except:
342                         import SCons.Defaults
343                         ENV = SCons.Defaults.ConstructionEnvironment['ENV']
344                     ret = spawn(cmd_line[0], cmd_line, ENV)
345                     if ret:
346                         return ret
347         return 0
348
349     def _sig_dict(self, kw):
350         """Supply a dictionary for use in computing signatures.
351
352         For signature purposes, it doesn't matter what targets or
353         sources we use, so long as we use the same ones every time
354         so the signature stays the same.  We supply an array of two
355         of each to allow for distinction between TARGET and TARGETS.
356         """
357         kw['target'] = ['__t1__', '__t2__']
358         kw['source'] = ['__s1__', '__s2__']
359         return apply(self.subst_dict, (), kw)
360
361     def get_raw_contents(self, **kw):
362         """Return the complete contents of this action's command line.
363         """
364         return SCons.Util.scons_subst(string.join(self.cmd_list),
365                                       self._sig_dict(kw), {})
366
367     def get_contents(self, **kw):
368         """Return the signature contents of this action's command line.
369
370         This strips $(-$) and everything in between the string,
371         since those parts don't affect signatures.
372         """
373         return SCons.Util.scons_subst(string.join(self.cmd_list),
374                                       self._sig_dict(kw), {}, _remove)
375
376 class CommandGeneratorAction(ActionBase):
377     """Class for command-generator actions."""
378     def __init__(self, generator):
379         self.generator = generator
380
381     def __generate(self, kw):
382         import SCons.Util
383
384         # Wrap the environment dictionary in an EnvDictProxy
385         # object to make variable interpolation easier for the
386         # client.
387         args = copy.copy(kw)
388         if args.has_key("env") and not isinstance(args["env"], EnvDictProxy):
389             args["env"] = EnvDictProxy(args["env"])
390
391         # ensure that target is a list, to make it easier to write
392         # generator functions:
393         if args.has_key("target") and not SCons.Util.is_List(args["target"]):
394             args["target"] = [args["target"]]
395
396         ret = apply(self.generator, (), args)
397         gen_cmd = Action(ret)
398         if not gen_cmd:
399             raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
400         return gen_cmd
401
402     def execute(self, **kw):
403         return apply(self.__generate(kw).execute, (), kw)
404
405     def get_contents(self, **kw):
406         """Return the signature contents of this action's command line.
407
408         This strips $(-$) and everything in between the string,
409         since those parts don't affect signatures.
410         """
411         return apply(self.__generate(kw).get_contents, (), kw)
412
413 class FunctionAction(ActionBase):
414     """Class for Python function actions."""
415     def __init__(self, function):
416         self.function = function
417
418     def execute(self, **kw):
419         # if print_actions:
420         # XXX:  WHAT SHOULD WE PRINT HERE?
421         if execute_actions:
422             if kw.has_key('target') and not \
423                SCons.Util.is_List(kw['target']):
424                 kw['target'] = [ kw['target'] ]
425             if kw.has_key('source') and not \
426                SCons.Util.is_List(kw['source']):
427                 kw['source'] = [ kw['source'] ]
428             if kw.has_key("env") and not isinstance(kw["env"], EnvDictProxy):
429                 kw["env"] = EnvDictProxy(kw["env"])
430             return apply(self.function, (), kw)
431
432     def get_contents(self, **kw):
433         """Return the signature contents of this callable action.
434
435         By providing direct access to the code object of the
436         function, Python makes this extremely easy.  Hooray!
437         """
438         #XXX DOES NOT ACCOUNT FOR CHANGES IN ENVIRONMENT VARIABLES
439         #THE FUNCTION MAY USE
440         try:
441             # "self.function" is a function.
442             code = self.function.func_code.co_code
443         except:
444             # "self.function" is a callable object.
445             code = self.function.__call__.im_func.func_code.co_code
446         return str(code)
447
448 class ListAction(ActionBase):
449     """Class for lists of other actions."""
450     def __init__(self, list):
451         self.list = map(lambda x: Action(x), list)
452
453     def execute(self, **kw):
454         for l in self.list:
455             r = apply(l.execute, (), kw)
456             if r != 0:
457                 return r
458         return 0
459
460     def get_contents(self, **kw):
461         """Return the signature contents of this action list.
462
463         Simple concatenation of the signatures of the elements.
464         """
465
466         return reduce(lambda x, y: x + str(y.get_contents()), self.list, "")