"""SCons.Util Various utility functions go here. """ # # __COPYRIGHT__ # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import copy import os import os.path import re import stat import string import sys import types import UserDict import UserList try: from UserString import UserString except ImportError: class UserString: pass _altsep = os.altsep if _altsep is None and sys.platform == 'win32': # My ActivePython 2.0.1 doesn't set os.altsep! What gives? _altsep = '/' def splitext(path): "Same as os.path.splitext() but faster." if _altsep: sep = max(string.rfind(path, os.sep), string.rfind(path, _altsep)) else: sep = string.rfind(path, os.sep) dot = string.rfind(path, '.') if dot > sep: return path[:dot],path[dot:] else: return path,"" def updrive(path): """ Make the drive letter (if any) upper case. This is useful because Windows is inconsitent on the case of the drive letter, which can cause inconsistencies when calculating command signatures. """ drive, rest = os.path.splitdrive(path) if drive: path = string.upper(drive) + rest return path class Literal: """A wrapper for a string. If you use this object wrapped around a string, then it will be interpreted as literal. When passed to the command interpreter, all special characters will be escaped.""" def __init__(self, lstr): self.lstr = lstr def __str__(self): return self.lstr def is_literal(self): return 1 class PathList(UserList.UserList): """This class emulates the behavior of a list, but also implements the special "path dissection" attributes we can use to find suffixes, base names, etc. of the paths in the list. One other special attribute of this class is that, by overriding the __str__ and __repr__ methods, this class represents itself as a space-concatenated string of the list elements, as in: >>> pl=PathList(["/foo/bar.txt", "/baz/foo.txt"]) >>> pl '/foo/bar.txt /baz/foo.txt' >>> pl.base 'bar foo' """ def __init__(self, seq = []): UserList.UserList.__init__(self, seq) def __getattr__(self, name): # This is how we implement the "special" attributes # such as base, suffix, basepath, etc. try: return self.dictSpecialAttrs[name](self) except KeyError: raise AttributeError, 'PathList has no attribute: %s' % name def __splitPath(self, split_func=os.path.split): """This method calls the supplied split_func on each element in the contained list. We expect split_func to return a 2-tuple, usually representing two elements of a split file path, such as those returned by os.path.split(). We return a 2-tuple of lists, each equal in length to the contained list. The first list represents all the elements from the first part of the split operation, the second represents all elements from the second part.""" list1 = [] list2 = [] for strPath in self.data: first_part, second_part = split_func(strPath) list1.append(first_part) list2.append(second_part) # Note that we return explicit PathList() instances, not # self.__class__(). This makes sure the right attributes are # available even if this object is a Lister, not a PathList. return (PathList(list1), PathList(list2)) def __getBasePath(self): """Return the file's directory and file name, with the suffix stripped.""" return self.__splitPath(splitext)[0] def __getSuffix(self): """Return the file's suffix.""" return self.__splitPath(splitext)[1] def __getFileName(self): """Return the file's name without the path.""" return self.__splitPath()[1] def __getDir(self): """Return the file's path.""" return self.__splitPath()[0] def __getBase(self): """Return the file name with path and suffix stripped.""" return self.__getFileName().__splitPath(splitext)[0] def __getAbsPath(self): """Return the absolute path""" # Note that we return an explicit PathList() instance, not # self.__class__(). This makes sure the right attributes are # available even if this object is a Lister, not a PathList. return PathList(map(lambda x: updrive(os.path.abspath(x)), self.data)) dictSpecialAttrs = { "file" : __getFileName, "base" : __getBasePath, "filebase" : __getBase, "dir" : __getDir, "suffix" : __getSuffix, "abspath" : __getAbsPath} def is_literal(self): return 1 def __str__(self): return string.join(self.data) def to_String(self): # Used by our variable-interpolation to interpolate a string. # The interpolation doesn't use __str__() for this because then # it interpolates other lists as "['x', 'y']". return string.join(self.data) def __repr__(self): return repr(string.join(self.data)) def __getitem__(self, item): # We must do this to ensure that single items returned # by index access have the special attributes such as # suffix and basepath. return self.__class__([ UserList.UserList.__getitem__(self, item), ]) _env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[_a-zA-Z]\w*})$') def get_environment_var(varstr): """Given a string, first determine if it looks like a reference to a single environment variable, like "$FOO" or "${FOO}". If so, return that variable with no decorations ("FOO"). If not, return None.""" mo=_env_var.match(to_String(varstr)) if mo: var = mo.group(1) if var[0] == '{': return var[1:-1] else: return var else: return None def quote_spaces(arg): if ' ' in arg or '\t' in arg: return '"%s"' % arg else: return str(arg) # Several functions below deal with Environment variable # substitution. Part of this process involves inserting # a bunch of special escape sequences into the string # so that when we are all done, we know things like # where to split command line args, what strings to # interpret literally, etc. A dictionary of these # sequences follows: # # \0\1 signifies a division between arguments in # a command line. # # \0\2 signifies a division between multiple distinct # commands, i.e., a newline # # \0\3 This string should be interpreted literally. # This code occurring anywhere in the string means # the whole string should have all special characters # escaped. # # \0\4 A literal dollar sign '$' # # \0\5 Placed before and after interpolated variables # so that we do not accidentally smush two variables # together during the recursive interpolation process. _cv = re.compile(r'\$([_a-zA-Z]\w*|{[^}]*})') _space_sep = re.compile(r'[\t ]+(?![^{]*})') _newline = re.compile(r'[\r\n]+') def _convertArg(x): """This function converts an individual argument. If the argument is to be interpreted literally, with all special characters escaped, then we insert a special code in front of it, so that the command interpreter will know this.""" literal = 0 try: if x.is_literal(): literal = 1 except AttributeError: pass if not literal: # escape newlines as '\0\2', '\0\1' denotes an argument split # Also escape double-dollar signs to mean the literal dollar sign. return string.replace(_newline.sub('\0\2', to_String(x)), '$$', '\0\4') else: # Interpret non-string args as literals. # The special \0\3 code will tell us to encase this string # in a Literal instance when we are all done # Also escape out any $ signs because we don't want # to continue interpolating a literal. return '\0\3' + string.replace(str(x), '$', '\0\4') def _convert(x): """This function is used to convert construction variable values or the value of strSubst to a string for interpolation. This function follows the rules outlined in the documentaion for scons_subst_list()""" if x is None: return '' elif is_String(x): # escape newlines as '\0\2', '\0\1' denotes an argument split return _convertArg(_space_sep.sub('\0\1', x)) elif is_List(x): # '\0\1' denotes an argument split return string.join(map(_convertArg, x), '\0\1') else: return _convertArg(x) class CmdStringHolder: """This is a special class used to hold strings generated by scons_subst_list(). It defines a special method escape(). When passed a function with an escape algorithm for a particular platform, it will return the contained string with the proper escape sequences inserted.""" def __init__(self, cmd): """This constructor receives a string. The string can contain the escape sequence \0\3. If it does, then we will escape all special characters in the string before passing it to the command interpreter.""" self.data = cmd # Populate flatdata (the thing returned by str()) with the # non-escaped string self.escape(lambda x: x, lambda x: x) def __str__(self): """Return the string in its current state.""" return self.flatdata def __len__(self): """Return the length of the string in its current state.""" return len(self.flatdata) def __getitem__(self, index): """Return the index'th element of the string in its current state.""" return self.flatdata[index] def escape(self, escape_func, quote_func=quote_spaces): """Escape the string with the supplied function. The function is expected to take an arbitrary string, then return it with all special characters escaped and ready for passing to the command interpreter. After calling this function, the next call to str() will return the escaped string. """ if string.find(self.data, '\0\3') >= 0: self.flatdata = escape_func(string.replace(self.data, '\0\3', '')) elif ' ' in self.data or '\t' in self.data: self.flatdata = quote_func(self.data) else: self.flatdata = self.data def __cmp__(self, rhs): return cmp(self.flatdata, str(rhs)) class DisplayEngine: def __init__(self): self.__call__ = self.print_it def print_it(self, text): print text def dont_print(self, text): pass def set_mode(self, mode): if mode: self.__call__ = self.print_it else: self.__call__ = self.dont_print def subst_dict(target, source, env): """Create a dictionary for substitution of construction variables. This translates the following special arguments: env - the construction environment itself, the values of which (CC, CCFLAGS, etc.) are copied straight into the dictionary target - the target (object or array of objects), used to generate the TARGET and TARGETS construction variables source - the source (object or array of objects), used to generate the SOURCES and SOURCE construction variables """ dict = env.Dictionary().copy() if not is_List(target): target = [target] dict['TARGETS'] = PathList(map(os.path.normpath, map(str, target))) if dict['TARGETS']: dict['TARGET'] = dict['TARGETS'][0] def rstr(x): try: return x.rstr() except AttributeError: return str(x) if not is_List(source): source = [source] dict['SOURCES'] = PathList(map(os.path.normpath, map(rstr, source))) if dict['SOURCES']: dict['SOURCE'] = dict['SOURCES'][0] return dict def scons_subst_list(strSubst, env, remove=None, target=None, source=None): """ This function serves the same purpose as scons_subst(), except this function returns the interpolated list as a list of lines, where each line is a list of command line arguments. In other words: The first (outer) list is a list of lines, where the substituted stirng has been broken along newline characters. The inner lists are lists of command line arguments, i.e., the argv array that should be passed to a spawn or exec function. There are a few simple rules this function follows in order to determine how to parse strSubst and construction variables into lines and arguments: 1) A string is interpreted as a space delimited list of arguments. 2) A list is interpreted as a list of arguments. This allows arguments with spaces in them to be expressed easily. 4) Anything that is not a list or string (e.g. a Node instance) is interpreted as a single argument, and is converted to a string. 3) Newline (\n) characters delimit lines. The newline parsing is done after all the other parsing, so it is not possible for arguments (e.g. file names) to contain embedded newline characters. """ if target != None: dict = subst_dict(target, source, env) else: dict = env.sig_dict() def repl(m, target=target, source=source, env=env, local_vars = dict, global_vars = { "__env__" : env }): key = m.group(1) if key[0] == '{': key = key[1:-1] try: e = eval(key, global_vars, local_vars) if callable(e): # We wait to evaluate callables until the end of everything # else. For now, we instert a special escape sequence # that we will look for later. return '\0\5' + _convert(e(target=target, source=source, env=env)) + '\0\5' else: # The \0\5 escape code keeps us from smushing two or more # variables together during recusrive substitution, i.e. # foo=$bar bar=baz barbaz=blat => $foo$bar->blat (bad) return "\0\5" + _convert(e) + "\0\5" except NameError: return '\0\5' # Convert the argument to a string: strSubst = _convert(strSubst) # Do the interpolation: n = 1 while n != 0: strSubst, n = _cv.subn(repl, strSubst) # Convert the interpolated string to a list of lines: listLines = string.split(strSubst, '\0\2') # Remove the patterns that match the remove argument: if remove: listLines = map(lambda x,re=remove: re.sub('', x), listLines) # Process escaped $'s and remove placeholder \0\5's listLines = map(lambda x: string.replace(string.replace(x, '\0\4', '$'), '\0\5', ''), listLines) # Finally split each line up into a list of arguments: return map(lambda x: map(CmdStringHolder, filter(lambda y:y, string.split(x, '\0\1'))), listLines) def scons_subst(strSubst, env, remove=None, target=None, source=None): """Recursively interpolates dictionary variables into the specified string, returning the expanded result. Variables are specified by a $ prefix in the string and begin with an initial underscore or alphabetic character followed by any number of underscores or alphanumeric characters. The construction variable names may be surrounded by curly braces to separate the name from trailing characters. """ # This function needs to be fast, so don't call scons_subst_list if target != None: dict = subst_dict(target, source, env) else: dict = env.sig_dict() def repl(m, target=target, source=source, env=env, local_vars = dict, global_vars = { '__env__' : env }): key = m.group(1) if key[0] == '{': key = key[1:-1] try: e = eval(key, global_vars, local_vars) if callable(e): e = e(target=target, source=source, env=env) if e is None: s = '' elif is_List(e): try: s = e.to_String() except AttributeError: s = string.join(map(to_String, e), ' ') else: s = to_String(e) except NameError: s = '' # Insert placeholders to avoid accidentally smushing # separate variables together. return "\0\5" + s + "\0\5" # Now, do the substitution n = 1 while n != 0: # escape double dollar signs strSubst = string.replace(strSubst, '$$', '\0\4') strSubst,n = _cv.subn(repl, strSubst) # and then remove remove if remove: strSubst = remove.sub('', strSubst) # Un-escape the string strSubst = string.replace(string.replace(strSubst, '\0\4', '$'), '\0\5', '') # strip out redundant white-space return string.strip(_space_sep.sub(' ', strSubst)) def render_tree(root, child_func, prune=0, margin=[0], visited={}): """ Render a tree of nodes into an ASCII tree view. root - the root node of the tree child_func - the function called to get the children of a node prune - don't visit the same node twice margin - the format of the left margin to use for children of root. 1 results in a pipe, and 0 results in no pipe. visited - a dictionary of visited nodes in the current branch if not prune, or in the whole tree if prune. """ if visited.has_key(root): return "" children = child_func(root) retval = "" for pipe in margin[:-1]: if pipe: retval = retval + "| " else: retval = retval + " " retval = retval + "+-" + str(root) + "\n" if not prune: visited = copy.copy(visited) visited[root] = 1 for i in range(len(children)): margin.append(i