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