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