c89e8e6bd1f219256632a42439ef7d09ed427b55
[scons.git] / src / engine / SCons / Util.py
1 """SCons.Util
2
3 Various utility functions go here.
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
33 import copy
34 import os
35 import os.path
36 import re
37 import stat
38 import string
39 import sys
40 import types
41 import UserDict
42 import UserList
43
44 try:
45     from UserString import UserString
46 except ImportError:
47     class UserString:
48         pass
49
50 _altsep = os.altsep
51 if _altsep is None and sys.platform == 'win32':
52     # My ActivePython 2.0.1 doesn't set os.altsep!  What gives?
53     _altsep = '/'
54
55 def splitext(path):
56     "Same as os.path.splitext() but faster."
57     if _altsep:
58         sep = max(string.rfind(path, os.sep), string.rfind(path, _altsep))
59     else:
60         sep = string.rfind(path, os.sep)
61     dot = string.rfind(path, '.')
62     if dot > sep:
63         return path[:dot],path[dot:]
64     else:
65         return path,""
66
67 def updrive(path):
68     """
69     Make the drive letter (if any) upper case.
70     This is useful because Windows is inconsitent on the case
71     of the drive letter, which can cause inconsistencies when
72     calculating command signatures.
73     """
74     drive, rest = os.path.splitdrive(path)
75     if drive:
76         path = string.upper(drive) + rest
77     return path
78
79 class Literal:
80     """A wrapper for a string.  If you use this object wrapped
81     around a string, then it will be interpreted as literal.
82     When passed to the command interpreter, all special
83     characters will be escaped."""
84     def __init__(self, lstr):
85         self.lstr = lstr
86
87     def __str__(self):
88         return self.lstr
89
90     def is_literal(self):
91         return 1
92
93 class PathList(UserList.UserList):
94     """This class emulates the behavior of a list, but also implements
95     the special "path dissection" attributes we can use to find
96     suffixes, base names, etc. of the paths in the list.
97
98     One other special attribute of this class is that, by
99     overriding the __str__ and __repr__ methods, this class
100     represents itself as a space-concatenated string of
101     the list elements, as in:
102
103     >>> pl=PathList(["/foo/bar.txt", "/baz/foo.txt"])
104     >>> pl
105     '/foo/bar.txt /baz/foo.txt'
106     >>> pl.base
107     'bar foo'
108     """
109     def __init__(self, seq = []):
110         UserList.UserList.__init__(self, seq)
111
112     def __getattr__(self, name):
113         # This is how we implement the "special" attributes
114         # such as base, suffix, basepath, etc.
115         try:
116             return self.dictSpecialAttrs[name](self)
117         except KeyError:
118             raise AttributeError, 'PathList has no attribute: %s' % name
119
120     def __splitPath(self, split_func=os.path.split):
121         """This method calls the supplied split_func on each element
122         in the contained list.  We expect split_func to return a
123         2-tuple, usually representing two elements of a split file path,
124         such as those returned by os.path.split().
125
126         We return a 2-tuple of lists, each equal in length to the contained
127         list.  The first list represents all the elements from the
128         first part of the split operation, the second represents
129         all elements from the second part."""
130         list1 = []
131         list2 = []
132         for strPath in self.data:
133             first_part, second_part = split_func(strPath)
134             list1.append(first_part)
135             list2.append(second_part)
136         return (self.__class__(list1),
137                 self.__class__(list2))
138
139     def __getBasePath(self):
140         """Return the file's directory and file name, with the
141         suffix stripped."""
142         return self.__splitPath(splitext)[0]
143
144     def __getSuffix(self):
145         """Return the file's suffix."""
146         return self.__splitPath(splitext)[1]
147
148     def __getFileName(self):
149         """Return the file's name without the path."""
150         return self.__splitPath()[1]
151
152     def __getDir(self):
153         """Return the file's path."""
154         return self.__splitPath()[0]
155
156     def __getBase(self):
157         """Return the file name with path and suffix stripped."""
158         return self.__getFileName().__splitPath(splitext)[0]
159
160     def __getAbsPath(self):
161         """Return the absolute path"""
162         return map(lambda x: updrive(os.path.abspath(x)), self.data)
163
164     dictSpecialAttrs = { "file" : __getFileName,
165                          "base" : __getBasePath,
166                          "filebase" : __getBase,
167                          "dir" : __getDir,
168                          "suffix" : __getSuffix,
169                          "abspath" : __getAbsPath}
170
171     def is_literal(self):
172         return 1
173     
174     def __str__(self):
175         return string.join(self.data)
176
177     def __repr__(self):
178         return repr(string.join(self.data))
179
180     def __getitem__(self, item):
181         # We must do this to ensure that single items returned
182         # by index access have the special attributes such as
183         # suffix and basepath.
184         return self.__class__([ UserList.UserList.__getitem__(self, item), ])
185
186 _env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[^}]*})$')
187
188 def get_environment_var(varstr):
189     """Given a string, first determine if it looks like a reference
190     to a single environment variable, like "$FOO" or "${FOO}".
191     If so, return that variable with no decorations ("FOO").
192     If not, return None."""
193     mo=_env_var.match(to_String(varstr))
194     if mo:
195         var = mo.group(1)
196         if var[0] == '{':
197             return var[1:-1]
198         else:
199             return var
200     else:
201         return None
202
203 def quote_spaces(arg):
204     if ' ' in arg or '\t' in arg:
205         return '"%s"' % arg
206     else:
207         return str(arg)
208
209 # Several functions below deal with Environment variable
210 # substitution.  Part of this process involves inserting
211 # a bunch of special escape sequences into the string
212 # so that when we are all done, we know things like
213 # where to split command line args, what strings to
214 # interpret literally, etc.  A dictionary of these
215 # sequences follows:
216 #
217 # \0\1          signifies a division between arguments in
218 #               a command line.
219 #
220 # \0\2          signifies a division between multiple distinct
221 #               commands
222 #
223 # \0\3          This string should be interpreted literally.
224 #               This code occurring anywhere in the string means
225 #               the whole string should have all special characters
226 #               escaped.
227 #
228 # \0\4          A literal dollar sign '$'
229 #
230 # \0\5          Placed before and after interpolated variables
231 #               so that we do not accidentally smush to variables
232 #               together during the recursive interpolation process.
233
234 _cv = re.compile(r'\$([_a-zA-Z]\w*|{[^}]*})')
235 _space_sep = re.compile(r'[\t ]+(?![^{]*})')
236 _newline = re.compile(r'[\r\n]+')
237
238 def _convertArg(x):
239     """This function converts an individual argument.  If the
240     argument is to be interpreted literally, with all special
241     characters escaped, then we insert a special code in front
242     of it, so that the command interpreter will know this."""
243     literal = 0
244
245     try:
246         if x.is_literal():
247             literal = 1
248     except AttributeError:
249         pass
250     
251     if not literal:
252         # escape newlines as '\0\2', '\0\1' denotes an argument split
253         # Also escape double-dollar signs to mean the literal dollar sign.
254         return string.replace(_newline.sub('\0\2', to_String(x)), '$$', '\0\4')
255     else:
256         # Interpret non-string args as literals.
257         # The special \0\3 code will tell us to encase this string
258         # in a Literal instance when we are all done
259         # Also escape out any $ signs because we don't want
260         # to continue interpolating a literal.
261         return '\0\3' + string.replace(str(x), '$', '\0\4')
262
263 def _convert(x):
264     """This function is used to convert construction variable
265     values or the value of strSubst to a string for interpolation.
266     This function follows the rules outlined in the documentaion
267     for scons_subst_list()"""
268     if x is None:
269         return ''
270     elif is_String(x):
271         # escape newlines as '\0\2', '\0\1' denotes an argument split
272         return _convertArg(_space_sep.sub('\0\1', x))
273     elif is_List(x):
274         # '\0\1' denotes an argument split
275         return string.join(map(_convertArg, x), '\0\1')
276     else:
277         return _convertArg(x)
278
279 class CmdStringHolder:
280     """This is a special class used to hold strings generated
281     by scons_subst_list().  It defines a special method escape().
282     When passed a function with an escape algorithm for a
283     particular platform, it will return the contained string
284     with the proper escape sequences inserted."""
285
286     def __init__(self, cmd):
287         """This constructor receives a string.  The string
288         can contain the escape sequence \0\3.
289         If it does, then we will escape all special characters
290         in the string before passing it to the command interpreter."""
291         self.data = cmd
292         
293         # Populate flatdata (the thing returned by str()) with the
294         # non-escaped string
295         self.escape(lambda x: x, lambda x: x)
296
297     def __str__(self):
298         """Return the string in its current state."""
299         return self.flatdata
300
301     def __len__(self):
302         """Return the length of the string in its current state."""
303         return len(self.flatdata)
304
305     def __getitem__(self, index):
306         """Return the index'th element of the string in its current state."""
307         return self.flatdata[index]
308
309     def escape(self, escape_func, quote_func=quote_spaces):
310         """Escape the string with the supplied function.  The
311         function is expected to take an arbitrary string, then
312         return it with all special characters escaped and ready
313         for passing to the command interpreter.
314
315         After calling this function, the next call to str() will
316         return the escaped string.
317         """
318
319         if string.find(self.data, '\0\3') >= 0:
320             self.flatdata = escape_func(string.replace(self.data, '\0\3', ''))
321         elif ' ' in self.data or '\t' in self.data:
322             self.flatdata = quote_func(self.data)
323         else:
324             self.flatdata = self.data
325
326     def __cmp__(self, rhs):
327         return cmp(self.flatdata, str(rhs))
328         
329 class DisplayEngine:
330     def __init__(self):
331         self.__call__ = self.print_it
332
333     def print_it(self, text):
334         print text
335
336     def dont_print(self, text):
337         pass
338
339     def set_mode(self, mode):
340         if mode:
341             self.__call__ = self.print_it
342         else:
343             self.__call__ = self.dont_print
344
345
346 def scons_subst_list(strSubst, globals, locals, remove=None):
347     """
348     This function serves the same purpose as scons_subst(), except
349     this function returns the interpolated list as a list of lines, where
350     each line is a list of command line arguments. In other words:
351     The first (outer) list is a list of lines, where the
352     substituted stirng has been broken along newline characters.
353     The inner lists are lists of command line arguments, i.e.,
354     the argv array that should be passed to a spawn or exec
355     function.
356
357     There are a few simple rules this function follows in order to
358     determine how to parse strSubst and consruction variables into lines
359     and arguments:
360
361     1) A string is interpreted as a space delimited list of arguments.
362     2) A list is interpreted as a list of arguments. This allows arguments
363        with spaces in them to be expressed easily.
364     4) Anything that is not a list or string (e.g. a Node instance) is
365        interpreted as a single argument, and is converted to a string.
366     3) Newline (\n) characters delimit lines. The newline parsing is done
367        after all the other parsing, so it is not possible for arguments
368        (e.g. file names) to contain embedded newline characters.
369     """
370
371     def convert(x):
372         """This function is used to convert construction variable
373         values or the value of strSubst to a string for interpolation.
374         This function follows the rules outlined in the documentaion
375         for scons_subst_list()"""
376         if x is None:
377             return ''
378         elif is_String(x):
379             return _space_sep.sub('\0', x)
380         elif is_List(x):
381             return string.join(map(to_String, x), '\0')
382         else:
383             return to_String(x)
384
385     def repl(m, globals=globals, locals=locals):
386         key = m.group(1)
387         if key[0] == '{':
388             key = key[1:-1]
389         try:
390             e = eval(key, globals, locals)
391             # The \0\5 escape code keeps us from smushing two or more
392             # variables together during recusrive substitution, i.e.
393             # foo=$bar bar=baz barbaz=blat => $foo$bar->blat (bad)
394             return "\0\5" + _convert(e) + "\0\5"
395         except NameError:
396             return '\0\5'
397
398     # Convert the argument to a string:
399     strSubst = _convert(strSubst)
400
401     # Do the interpolation:
402     n = 1
403     while n != 0:
404         strSubst, n = _cv.subn(repl, strSubst)
405         
406     # Convert the interpolated string to a list of lines:
407     listLines = string.split(strSubst, '\0\2')
408
409     # Remove the patterns that match the remove argument: 
410     if remove:
411         listLines = map(lambda x,re=remove: re.sub('', x), listLines)
412
413     # Process escaped $'s and remove placeholder \0\5's
414     listLines = map(lambda x: string.replace(string.replace(x, '\0\4', '$'), '\0\5', ''), listLines)
415
416     # Finally split each line up into a list of arguments:
417     return map(lambda x: map(CmdStringHolder, filter(lambda y:y, string.split(x, '\0\1'))),
418                listLines)
419
420 def scons_subst(strSubst, globals, locals, remove=None):
421     """Recursively interpolates dictionary variables into
422     the specified string, returning the expanded result.
423     Variables are specified by a $ prefix in the string and
424     begin with an initial underscore or alphabetic character
425     followed by any number of underscores or alphanumeric
426     characters.  The construction variable names may be
427     surrounded by curly braces to separate the name from
428     trailing characters.
429     """
430
431     # This function needs to be fast, so don't call scons_subst_list
432
433     def repl(m, globals=globals, locals=locals):
434         key = m.group(1)
435         if key[0] == '{':
436             key = key[1:-1]
437         try:
438             e = eval(key, globals, locals)
439             if e is None:
440                 s = ''
441             elif is_List(e):
442                 s = string.join(map(to_String, e), ' ')
443             else:
444                 s = to_String(e)
445         except NameError:
446             s = ''
447         # Insert placeholders to avoid accidentally smushing
448         # separate variables together.
449         return "\0\5" + s + "\0\5"
450
451     # Now, do the substitution
452     n = 1
453     while n != 0:
454         # escape double dollar signs
455         strSubst = string.replace(strSubst, '$$', '\0\4')
456         strSubst,n = _cv.subn(repl, strSubst)
457     # and then remove remove
458     if remove:
459         strSubst = remove.sub('', strSubst)
460
461     # Un-escape the string
462     strSubst = string.replace(string.replace(strSubst, '\0\4', '$'),
463                               '\0\5', '')
464     # strip out redundant white-space
465     return string.strip(_space_sep.sub(' ', strSubst))
466
467 def render_tree(root, child_func, margin=[0], visited={}):
468     """
469     Render a tree of nodes into an ASCII tree view.
470     root - the root node of the tree
471     child_func - the function called to get the children of a node
472     margin - the format of the left margin to use for children of root.
473        1 results in a pipe, and 0 results in no pipe.
474     visited - a dictionart of visited nodes in the current branch
475     """
476
477     if visited.has_key(root):
478         return ""
479
480     children = child_func(root)
481     retval = ""
482     for pipe in margin[:-1]:
483         if pipe:
484             retval = retval + "| "
485         else:
486             retval = retval + "  "
487
488     retval = retval + "+-" + str(root) + "\n"
489     visited = copy.copy(visited)
490     visited[root] = 1
491
492     for i in range(len(children)):
493         margin.append(i<len(children)-1)
494         retval = retval + render_tree(children[i], child_func, margin, visited
495 )
496         margin.pop()
497
498     return retval
499
500 def is_Dict(e):
501     return type(e) is types.DictType or isinstance(e, UserDict.UserDict)
502
503 def is_List(e):
504     return type(e) is types.ListType or isinstance(e, UserList.UserList)
505
506 def to_String(s):
507     """Better than str() because it will preserve a unicode
508     object without converting it to ASCII."""
509     if hasattr(types, 'UnicodeType') and \
510        (type(s) is types.UnicodeType or \
511         (isinstance(s, UserString) and type(s.data) is types.UnicodeType)):
512         return unicode(s)
513     else:
514         return str(s)
515
516 def argmunge(arg):
517     return Split(arg)
518
519 def Split(arg):
520     """This function converts a string or list into a list of strings
521     or Nodes.  This makes things easier for users by allowing files to
522     be specified as a white-space separated list to be split.
523     The input rules are:
524         - A single string containing names separated by spaces. These will be
525           split apart at the spaces.
526         - A single Node instance
527         - A list containing either strings or Node instances. Any strings
528           in the list are not split at spaces.
529     In all cases, the function returns a list of Nodes and strings."""
530     if is_List(arg):
531         return arg
532     elif is_String(arg):
533         return string.split(arg)
534     else:
535         return [arg]
536
537 def mapPaths(paths, dir, env=None):
538     """Takes a single node or string, or a list of nodes and/or
539     strings.  We leave the nodes untouched, but we put the strings
540     under the supplied directory node dir, if they are not an absolute
541     path.
542
543     For instance, the following:
544
545     n = SCons.Node.FS.default_fs.File('foo')
546     mapPaths([ n, 'foo', '/bar' ],
547              SCons.Node.FS.default_fs.Dir('baz'), env)
548
549     ...would return:
550
551     [ n, 'baz/foo', '/bar' ]
552
553     The env argument, if given, is used to perform variable
554     substitution on the source string(s).
555     """
556
557     def mapPathFunc(path, dir=dir, env=env):
558         if is_String(path):
559             if env:
560                 path = env.subst(path)
561             if dir:
562                 if not path:
563                     return str(dir)
564                 if os.path.isabs(path) or path[0] == '#':
565                     return path
566                 return dir.path_ + path
567         return path
568
569     if not is_List(paths):
570         paths = [ paths ]
571     ret = map(mapPathFunc, paths)
572     return ret
573     
574
575 if hasattr(types, 'UnicodeType'):
576     def is_String(e):
577         return type(e) is types.StringType \
578             or type(e) is types.UnicodeType \
579             or isinstance(e, UserString)
580 else:
581     def is_String(e):
582         return type(e) is types.StringType or isinstance(e, UserString)
583
584 class Proxy:
585     """A simple generic Proxy class, forwarding all calls to
586     subject.  Inherit from this class to create a Proxy."""
587     def __init__(self, subject):
588         self.__subject = subject
589         
590     def __getattr__(self, name):
591         return getattr(self.__subject, name)
592
593 # attempt to load the windows registry module:
594 can_read_reg = 0
595 try:
596     import _winreg
597
598     can_read_reg = 1
599     hkey_mod = _winreg
600
601     RegOpenKeyEx = _winreg.OpenKeyEx
602     RegEnumKey = _winreg.EnumKey
603     RegEnumValue = _winreg.EnumValue
604     RegQueryValueEx = _winreg.QueryValueEx
605     RegError = _winreg.error
606
607 except ImportError:
608     try:
609         import win32api
610         import win32con
611         can_read_reg = 1
612         hkey_mod = win32con
613
614         RegOpenKeyEx = win32api.RegOpenKeyEx
615         RegEnumKey = win32api.RegEnumKey
616         RegEnumValue = win32api.RegEnumValue
617         RegQueryValueEx = win32api.RegQueryValueEx
618         RegError = win32api.error
619
620     except ImportError:
621         pass
622
623 if can_read_reg:
624     HKEY_CLASSES_ROOT = hkey_mod.HKEY_CLASSES_ROOT
625     HKEY_LOCAL_MACHINE = hkey_mod.HKEY_LOCAL_MACHINE
626     HKEY_CURRENT_USER = hkey_mod.HKEY_CURRENT_USER
627     HKEY_USERS = hkey_mod.HKEY_USERS
628
629
630 if sys.platform == 'win32':
631
632     def WhereIs(file, path=None, pathext=None):
633         if path is None:
634             path = os.environ['PATH']
635         if is_String(path):
636             path = string.split(path, os.pathsep)
637         if pathext is None:
638             try:
639                 pathext = os.environ['PATHEXT']
640             except KeyError:
641                 pathext = '.COM;.EXE;.BAT;.CMD'
642         if is_String(pathext):
643             pathext = string.split(pathext, os.pathsep)
644         for ext in pathext:
645             if string.lower(ext) == string.lower(file[-len(ext):]):
646                 pathext = ['']
647                 break
648         for dir in path:
649             f = os.path.join(dir, file)
650             for ext in pathext:
651                 fext = f + ext
652                 if os.path.isfile(fext):
653                     return fext
654         return None
655
656 elif os.name == 'os2':
657
658     def WhereIs(file, path=None, pathext=None):
659         if path is None:
660             path = os.environ['PATH']
661         if is_String(path):
662             path = string.split(path, os.pathsep)
663         if pathext is None:
664             pathext = ['.exe', '.cmd']
665         for ext in pathext:
666             if string.lower(ext) == string.lower(file[-len(ext):]):
667                 pathext = ['']
668                 break
669         for dir in path:
670             f = os.path.join(dir, file)
671             for ext in pathext:
672                 fext = f + ext
673                 if os.path.isfile(fext):
674                     return fext
675         return None
676
677 else:
678
679     def WhereIs(file, path=None, pathext=None):
680         if path is None:
681             path = os.environ['PATH']
682         if is_String(path):
683             path = string.split(path, os.pathsep)
684         for dir in path:
685             f = os.path.join(dir, file)
686             if os.path.isfile(f):
687                 try:
688                     st = os.stat(f)
689                 except:
690                     continue
691                 if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
692                     return f
693         return None
694
695 def ParseConfig(env, command, function=None):
696     """Use the specified function to parse the output of the command in order
697     to modify the specified environment. The 'command' can be a string or a
698     list of strings representing a command and it's arguments. 'Function' is
699     an optional argument that takes the environment and the output of the
700     command. If no function is specified, the output will be treated as the
701     output of a typical 'X-config' command (i.e. gtk-config) and used to set
702     the CPPPATH, LIBPATH, LIBS, and CCFLAGS variables.
703     """
704     # the default parse function
705     def parse_conf(env, output):
706         env_dict = env.Dictionary()
707         static_libs = []
708
709         # setup all the dictionary options
710         if not env_dict.has_key('CPPPATH'):
711             env_dict['CPPPATH'] = []
712         if not env_dict.has_key('LIBPATH'):
713             env_dict['LIBPATH'] = []
714         if not env_dict.has_key('LIBS'):
715             env_dict['LIBS'] = []
716         if not env_dict.has_key('CCFLAGS') or env_dict['CCFLAGS'] == "":
717             env_dict['CCFLAGS'] = []
718
719         params = string.split(output)
720         for arg in params:
721             switch = arg[0:1]
722             opt = arg[1:2]
723             if switch == '-':
724                 if opt == 'L':
725                     env_dict['LIBPATH'].append(arg[2:])
726                 elif opt == 'l':
727                     env_dict['LIBS'].append(arg[2:])
728                 elif opt == 'I':
729                     env_dict['CPPPATH'].append(arg[2:])
730                 else:
731                     env_dict['CCFLAGS'].append(arg)
732             else:
733                 static_libs.append(arg)
734         return static_libs
735
736     if function is None:
737         function = parse_conf
738     if type(command) is type([]):
739         command = string.join(command)
740     return function(env, os.popen(command).read())
741
742 def dir_index(directory):
743     files = []
744     for file in os.listdir(directory):
745         fullname = os.path.join(directory, file)
746         files.append(fullname)
747     return files
748
749 def fs_delete(path, remove=1):
750     try:
751         if os.path.exists(path):
752             if os.path.isfile(path):
753                 if remove: os.unlink(path)
754                 display("Removed " + path)
755             elif os.path.isdir(path) and not os.path.islink(path):
756                 # delete everything in the dir
757                 for p in dir_index(path):
758                     if os.path.isfile(p):
759                         if remove: os.unlink(p)
760                         display("Removed " + p)
761                     else:
762                         fs_delete(p, remove)
763                 # then delete dir itself
764                 if remove: os.rmdir(path)
765                 display("Removed directory " + path)
766     except OSError, e:
767         print "scons: Could not remove '%s':" % str(path), e.strerror
768
769 display = DisplayEngine()