llmanifest.py

Go to the documentation of this file.
00001 """\
00002 @file llmanifest.py
00003 @author Ryan Williams
00004 @brief Library for specifying operations on a set of files.
00005 
00006 $LicenseInfo:firstyear=2007&license=mit$
00007 
00008 Copyright (c) 2007-2008, Linden Research, Inc.
00009 
00010 Permission is hereby granted, free of charge, to any person obtaining a copy
00011 of this software and associated documentation files (the "Software"), to deal
00012 in the Software without restriction, including without limitation the rights
00013 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
00014 copies of the Software, and to permit persons to whom the Software is
00015 furnished to do so, subject to the following conditions:
00016 
00017 The above copyright notice and this permission notice shall be included in
00018 all copies or substantial portions of the Software.
00019 
00020 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
00021 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
00022 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
00023 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
00024 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
00025 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
00026 THE SOFTWARE.
00027 $/LicenseInfo$
00028 """
00029 
00030 import commands
00031 import errno
00032 import filecmp
00033 import fnmatch
00034 import getopt
00035 import glob
00036 import os
00037 import os.path
00038 import re
00039 import shutil
00040 import sys
00041 import tarfile
00042 import errno
00043 
00044 def path_ancestors(path):
00045     path = os.path.normpath(path)
00046     result = []
00047     while len(path) > 0:
00048         result.append(path)
00049         path, sub = os.path.split(path)
00050     return result
00051 
00052 def proper_windows_path(path, current_platform = sys.platform):
00053     """ This function takes an absolute Windows or Cygwin path and
00054     returns a path appropriately formatted for the platform it's
00055     running on (as determined by sys.platform)"""
00056     path = path.strip()
00057     drive_letter = None
00058     rel = None
00059     match = re.match("/cygdrive/([a-z])/(.*)", path)
00060     if(not match):
00061         match = re.match('([a-zA-Z]):\\\(.*)', path)
00062     if(not match):
00063         return None         # not an absolute path
00064     drive_letter = match.group(1)
00065     rel = match.group(2)
00066     if(current_platform == "cygwin"):
00067         return "/cygdrive/" + drive_letter.lower() + '/' + rel.replace('\\', '/')
00068     else:
00069         return drive_letter.upper() + ':\\' + rel.replace('/', '\\')
00070 
00071 def get_default_platform(dummy):
00072     return {'linux2':'linux',
00073             'linux1':'linux',
00074             'cygwin':'windows',
00075             'win32':'windows',
00076             'darwin':'darwin'
00077             }[sys.platform]
00078 
00079 def get_default_version(srctree):
00080     # look up llversion.h and parse out the version info
00081     paths = [os.path.join(srctree, x, 'llversionviewer.h') for x in ['llcommon', '../llcommon', '../../indra/llcommon.h']]
00082     for p in paths:
00083         if os.path.exists(p):
00084             contents = open(p, 'r').read()
00085             major = re.search("LL_VERSION_MAJOR\s=\s([0-9]+)", contents).group(1)
00086             minor = re.search("LL_VERSION_MINOR\s=\s([0-9]+)", contents).group(1)
00087             patch = re.search("LL_VERSION_PATCH\s=\s([0-9]+)", contents).group(1)
00088             build = re.search("LL_VERSION_BUILD\s=\s([0-9]+)", contents).group(1)
00089             return major, minor, patch, build
00090 
00091 def get_channel(srctree):
00092     # look up llversionserver.h and parse out the version info
00093     paths = [os.path.join(srctree, x, 'llversionviewer.h') for x in ['llcommon', '../llcommon', '../../indra/llcommon.h']]
00094     for p in paths:
00095         if os.path.exists(p):
00096             contents = open(p, 'r').read()
00097             channel = re.search("LL_CHANNEL\s=\s\"(.+)\";\s*$", contents, flags = re.M).group(1)
00098             return channel
00099     
00100 
00101 DEFAULT_CHANNEL = 'Second Life Release'
00102 
00103 ARGUMENTS=[
00104     dict(name='actions',
00105          description="""This argument specifies the actions that are to be taken when the
00106         script is run.  The meaningful actions are currently:
00107           copy     - copies the files specified by the manifest into the
00108                      destination directory.
00109           package  - bundles up the files in the destination directory into
00110                      an installer for the current platform
00111           unpacked - bundles up the files in the destination directory into
00112                      a simple tarball
00113         Example use: %(name)s --actions="copy unpacked" """,
00114          default="copy package"),
00115     dict(name='arch',
00116          description="""This argument is appended to the platform string for
00117         determining which manifest class to run.
00118         Example use: %(name)s --arch=i686
00119         On Linux this would try to use Linux_i686Manifest.""",
00120          default=""),
00121     dict(name='configuration',
00122          description="""The build configuration used. Only used on OS X for
00123         now, but it could be used for other platforms as well.""",
00124          default="Universal"),
00125     dict(name='grid',
00126          description="""Which grid the client will try to connect to. Even
00127         though it's not strictly a grid, 'firstlook' is also an acceptable
00128         value for this parameter.""",
00129          default=""),
00130     dict(name='channel',
00131          description="""The channel to use for updates, packaging, settings name, etc.""",
00132          default=get_channel),
00133     dict(name='login_channel',
00134          description="""The channel to use for login handshake/updates only.""",
00135          default=None),
00136     dict(name='installer_name',
00137          description=""" The name of the file that the installer should be
00138         packaged up into. Only used on Linux at the moment.""",
00139          default=None),
00140     dict(name='login_url',
00141          description="""The url that the login screen displays in the client.""",
00142          default=None),
00143     dict(name='platform',
00144          description="""The current platform, to be used for looking up which
00145         manifest class to run.""",
00146          default=get_default_platform),
00147     dict(name='version',
00148          description="""This specifies the version of Second Life that is
00149         being packaged up.""",
00150          default=get_default_version)
00151     ]
00152 
00153 def usage(srctree=""):
00154     nd = {'name':sys.argv[0]}
00155     print """Usage:
00156     %(name)s [options] [destdir]
00157     Options:
00158     """ % nd
00159     for arg in ARGUMENTS:
00160         default = arg['default']
00161         if hasattr(default, '__call__'):
00162             default = "(computed value) \"" + str(default(srctree)) + '"'
00163         elif default is not None:
00164             default = '"' + default + '"'
00165         print "\t--%s        Default: %s\n\t%s\n" % (
00166             arg['name'],
00167             default,
00168             arg['description'] % nd)
00169 
00170 def main(argv=None, srctree='.', dsttree='./dst'):
00171     if(argv == None):
00172         argv = sys.argv
00173 
00174     option_names = [arg['name'] + '=' for arg in ARGUMENTS]
00175     option_names.append('help')
00176     options, remainder = getopt.getopt(argv[1:], "", option_names)
00177     if len(remainder) >= 1:
00178         dsttree = remainder[0]
00179 
00180     print "Source tree:", srctree
00181     print "Destination tree:", dsttree
00182 
00183     # convert options to a hash
00184     args = {}
00185     for opt in options:
00186         args[opt[0].replace("--", "")] = opt[1]
00187 
00188     # early out for help
00189     if args.has_key('help'):
00190         # *TODO: it is a huge hack to pass around the srctree like this
00191         usage(srctree)
00192         return
00193 
00194     # defaults
00195     for arg in ARGUMENTS:
00196         if not args.has_key(arg['name']):
00197             default = arg['default']
00198             if hasattr(default, '__call__'):
00199                 default = default(srctree)
00200             if default is not None:
00201                 args[arg['name']] = default
00202 
00203     # fix up version
00204     if args.has_key('version') and type(args['version']) == str:
00205         args['version'] = args['version'].split('.')
00206         
00207     # default and agni are default
00208     if args['grid'] in ['default', 'agni']:
00209         args['grid'] = ''
00210 
00211     if args.has_key('actions'):
00212         args['actions'] = args['actions'].split()
00213 
00214     # debugging
00215     for opt in args:
00216         print "Option:", opt, "=", args[opt]
00217 
00218     wm = LLManifest.for_platform(args['platform'], args.get('arch'))(srctree, dsttree, args)
00219     wm.do(*args['actions'])
00220     return 0
00221 
00222 class LLManifestRegistry(type):
00223     def __init__(cls, name, bases, dct):
00224         super(LLManifestRegistry, cls).__init__(name, bases, dct)
00225         match = re.match("(\w+)Manifest", name)
00226         if(match):
00227            cls.manifests[match.group(1).lower()] = cls
00228 
00229 class LLManifest(object):
00230     __metaclass__ = LLManifestRegistry
00231     manifests = {}
00232     def for_platform(self, platform, arch = None):
00233         if arch:
00234             platform = platform + '_' + arch
00235         return self.manifests[platform.lower()]
00236     for_platform = classmethod(for_platform)
00237 
00238     def __init__(self, srctree, dsttree, args):
00239         super(LLManifest, self).__init__()
00240         self.args = args
00241         self.file_list = []
00242         self.excludes = []
00243         self.actions = []
00244         self.src_prefix = [srctree]
00245         self.dst_prefix = [dsttree]
00246         self.created_paths = []
00247         
00248     def default_grid(self):
00249         return self.args.get('grid', None) == ''
00250     def default_channel(self):
00251         return self.args.get('channel', None) == DEFAULT_CHANNEL
00252 
00253     def construct(self):
00254         """ Meant to be overriden by LLManifest implementors with code that
00255         constructs the complete destination hierarchy."""
00256         pass # override this method
00257 
00258     def exclude(self, glob):
00259         """ Excludes all files that match the glob from being included
00260         in the file list by path()."""
00261         self.excludes.append(glob)
00262 
00263     def prefix(self, src='', dst=None):
00264         """ Pushes a prefix onto the stack.  Until end_prefix is
00265         called, all relevant method calls (esp. to path()) will prefix
00266         paths with the entire prefix stack.  Source and destination
00267         prefixes can be different, though if only one is provided they
00268         are both equal.  To specify a no-op, use an empty string, not
00269         None."""
00270         if(dst == None):
00271             dst = src
00272         self.src_prefix.append(src)
00273         self.dst_prefix.append(dst)
00274         return True  # so that you can wrap it in an if to get indentation
00275 
00276     def end_prefix(self, descr=None):
00277         """Pops a prefix off the stack.  If given an argument, checks
00278         the argument against the top of the stack.  If the argument
00279         matches neither the source or destination prefixes at the top
00280         of the stack, then misnesting must have occurred and an
00281         exception is raised."""
00282         # as an error-prevention mechanism, check the prefix and see if it matches the source or destination prefix.  If not, improper nesting may have occurred.
00283         src = self.src_prefix.pop()
00284         dst = self.dst_prefix.pop()
00285         if descr and not(src == descr or dst == descr):
00286             raise ValueError, "End prefix '" + descr + "' didn't match '" +src+ "' or '" +dst + "'"
00287 
00288     def get_src_prefix(self):
00289         """ Returns the current source prefix."""
00290         return os.path.join(*self.src_prefix)
00291 
00292     def get_dst_prefix(self):
00293         """ Returns the current destination prefix."""
00294         return os.path.join(*self.dst_prefix)
00295 
00296     def src_path_of(self, relpath):
00297         """Returns the full path to a file or directory specified
00298         relative to the source directory."""
00299         return os.path.join(self.get_src_prefix(), relpath)
00300 
00301     def dst_path_of(self, relpath):
00302         """Returns the full path to a file or directory specified
00303         relative to the destination directory."""
00304         return os.path.join(self.get_dst_prefix(), relpath)
00305 
00306     def ensure_src_dir(self, reldir):
00307         """Construct the path for a directory relative to the
00308         source path, and ensures that it exists.  Returns the
00309         full path."""
00310         path = os.path.join(self.get_src_prefix(), reldir)
00311         self.cmakedirs(path)
00312         return path
00313 
00314     def ensure_dst_dir(self, reldir):
00315         """Construct the path for a directory relative to the
00316         destination path, and ensures that it exists.  Returns the
00317         full path."""
00318         path = os.path.join(self.get_dst_prefix(), reldir)
00319         self.cmakedirs(path)
00320         return path
00321 
00322     def run_command(self, command):
00323         """ Runs an external command, and returns the output.  Raises
00324         an exception if the command reurns a nonzero status code.  For
00325         debugging/informational purpoases, prints out the command's
00326         output as it is received."""
00327         print "Running command:", command
00328         fd = os.popen(command, 'r')
00329         lines = []
00330         while True:
00331             lines.append(fd.readline())
00332             if(lines[-1] == ''):
00333                 break
00334             else:
00335                 print lines[-1],
00336         output = ''.join(lines)
00337         status = fd.close()
00338         if(status):
00339             raise RuntimeError(
00340                 "Command %s returned non-zero status (%s) \noutput:\n%s"
00341                 % (command, status, output) )
00342         return output
00343 
00344     def created_path(self, path):
00345         """ Declare that you've created a path in order to
00346           a) verify that you really have created it
00347           b) schedule it for cleanup"""
00348         if not os.path.exists(path):
00349             raise RuntimeError, "Should be something at path " + path
00350         self.created_paths.append(path)
00351 
00352     def put_in_file(self, contents, dst):
00353         # write contents as dst
00354         f = open(self.dst_path_of(dst), "wb")
00355         f.write(contents)
00356         f.close()
00357 
00358     def replace_in(self, src, dst=None, searchdict={}):
00359         if(dst == None):
00360             dst = src
00361         # read src
00362         f = open(self.src_path_of(src), "rbU")
00363         contents = f.read()
00364         f.close()
00365         # apply dict replacements
00366         for old, new in searchdict.iteritems():
00367             contents = contents.replace(old, new)
00368         self.put_in_file(contents, dst)
00369         self.created_paths.append(dst)
00370 
00371     def copy_action(self, src, dst):
00372         if(src and (os.path.exists(src) or os.path.islink(src))):
00373             # ensure that destination path exists
00374             self.cmakedirs(os.path.dirname(dst))
00375             self.created_paths.append(dst)
00376             if(not os.path.isdir(src)):
00377                 self.ccopy(src,dst)
00378             else:
00379                 # src is a dir
00380                 self.ccopytree(src,dst)
00381         else:
00382             print "Doesn't exist:", src
00383 
00384     def package_action(self, src, dst):
00385         pass
00386 
00387     def copy_finish(self):
00388         pass
00389 
00390     def package_finish(self):
00391         pass
00392 
00393     def unpacked_finish(self):
00394         unpacked_file_name = "unpacked_%(plat)s_%(vers)s.tar" % {
00395             'plat':self.args['platform'],
00396             'vers':'_'.join(self.args['version'])}
00397         print "Creating unpacked file:", unpacked_file_name
00398         # could add a gz here but that doubles the time it takes to do this step
00399         tf = tarfile.open(self.src_path_of(unpacked_file_name), 'w:')
00400         # add the entire installation package, at the very top level
00401         tf.add(self.get_dst_prefix(), "")
00402         tf.close()
00403 
00404     def cleanup_finish(self):
00405         """ Delete paths that were specified to have been created by this script"""
00406         for c in self.created_paths:
00407             # *TODO is this gonna be useful?
00408             print "Cleaning up " + c
00409 
00410     def process_file(self, src, dst):
00411         if(self.includes(src, dst)):
00412 #            print src, "=>", dst
00413             for action in self.actions:
00414                 methodname = action + "_action"
00415                 method = getattr(self, methodname, None)
00416                 if method is not None:
00417                     method(src, dst)
00418             self.file_list.append([src, dst])
00419         else:
00420             print "Excluding: ", src, dst
00421 
00422 
00423     def process_directory(self, src, dst):
00424         if(not self.includes(src, dst)):
00425             print "Excluding: ", src, dst
00426             return
00427         names = os.listdir(src)
00428         self.cmakedirs(dst)
00429         errors = []
00430         for name in names:
00431             srcname = os.path.join(src, name)
00432             dstname = os.path.join(dst, name)
00433             if os.path.isdir(srcname):
00434                 self.process_directory(srcname, dstname)
00435             else:
00436                 self.process_file(srcname, dstname)
00437 
00438 
00439 
00440     def includes(self, src, dst):
00441         if src:
00442             for excl in self.excludes:
00443                 if fnmatch.fnmatch(src, excl):
00444                     return False
00445         return True
00446 
00447     def remove(self, *paths):
00448         for path in paths:
00449             if(os.path.exists(path)):
00450                 print "Removing path", path
00451                 if(os.path.isdir(path)):
00452                     shutil.rmtree(path)
00453                 else:
00454                     os.remove(path)
00455 
00456     def ccopy(self, src, dst):
00457         """ Copy a single file or symlink.  Uses filecmp to skip copying for existing files."""
00458         if os.path.islink(src):
00459             linkto = os.readlink(src)
00460             if(os.path.islink(dst) or os.path.exists(dst)):
00461                 os.remove(dst)  # because symlinking over an existing link fails
00462             os.symlink(linkto, dst)
00463         else:
00464             # Don't recopy file if it's up-to-date.
00465             # If we seem to be not not overwriting files that have been
00466             # updated, set the last arg to False, but it will take longer.
00467             if(os.path.exists(dst) and filecmp.cmp(src, dst, True)):
00468                 return
00469             # only copy if it's not excluded
00470             if(self.includes(src, dst)):
00471                 try:
00472                     os.unlink(dst)
00473                 except OSError, err:
00474                     if err.errno != errno.ENOENT:
00475                         raise
00476 
00477                 shutil.copy2(src, dst)
00478 
00479     def ccopytree(self, src, dst):
00480         """Direct copy of shutil.copytree with the additional
00481         feature that the destination directory can exist.  It
00482         is so dumb that Python doesn't come with this. Also it
00483         implements the excludes functionality."""
00484         if(not self.includes(src, dst)):
00485             return
00486         names = os.listdir(src)
00487         self.cmakedirs(dst)
00488         errors = []
00489         for name in names:
00490             srcname = os.path.join(src, name)
00491             dstname = os.path.join(dst, name)
00492             try:
00493                 if os.path.isdir(srcname):
00494                     self.ccopytree(srcname, dstname)
00495                 else:
00496                     self.ccopy(srcname, dstname)
00497                     # XXX What about devices, sockets etc.?
00498             except (IOError, os.error), why:
00499                 errors.append((srcname, dstname, why))
00500         if errors:
00501             raise RuntimeError, errors
00502 
00503 
00504     def cmakedirs(self, path):
00505         """Ensures that a directory exists, and doesn't throw an exception
00506         if you call it on an existing directory."""
00507 #        print "making path: ", path
00508         path = os.path.normpath(path)
00509         self.created_paths.append(path)
00510         if not os.path.exists(path):
00511             os.makedirs(path)
00512 
00513     def find_existing_file(self, *list):
00514         for f in list:
00515             if(os.path.exists(f)):
00516                 return f
00517         # didn't find it, return last item in list
00518         if len(list) > 0:
00519             return list[-1]
00520         else:
00521             return None
00522 
00523     def contents_of_tar(self, src_tar, dst_dir):
00524         """ Extracts the contents of the tarfile (specified
00525         relative to the source prefix) into the directory
00526         specified relative to the destination directory."""
00527         self.check_file_exists(src_tar)
00528         tf = tarfile.open(self.src_path_of(src_tar), 'r')
00529         for member in tf.getmembers():
00530             tf.extract(member, self.ensure_dst_dir(dst_dir))
00531             # TODO get actions working on these dudes, perhaps we should extract to a temporary directory and then process_directory on it?
00532             self.file_list.append([src_tar,
00533                            self.dst_path_of(os.path.join(dst_dir,member.name))])
00534         tf.close()
00535 
00536 
00537     def wildcard_regex(self, src_glob, dst_glob):
00538  #       print "regex_pair:", src_glob, dst_glob
00539         src_re = re.escape(src_glob)
00540         src_re = src_re.replace('\*', '([-a-zA-Z0-9._ ]+)')
00541         dst_temp = dst_glob
00542         i = 1
00543         while(dst_temp.count("*") > 0):
00544             dst_temp = dst_temp.replace('*', '\g<' + str(i) + '>', 1)
00545             i = i+1
00546  #       print "regex_result:", src_re, dst_temp
00547         return re.compile(src_re), dst_temp
00548 
00549     def check_file_exists(self, path):
00550         if(not os.path.exists(path) and not os.path.islink(path)):
00551             raise RuntimeError("Path %s doesn't exist" % (
00552                 os.path.normpath(os.path.join(os.getcwd(), path)),))
00553 
00554 
00555     wildcard_pattern = re.compile('\*')
00556     def expand_globs(self, src, dst):
00557         def fw_slash(str):
00558             return str.replace('\\', '/')
00559         def os_slash(str):
00560             return str.replace('/', os.path.sep)
00561         dst = fw_slash(dst)
00562         src = fw_slash(src)
00563         src_list = glob.glob(src)
00564         src_re, d_template = self.wildcard_regex(src, dst)
00565         for s in src_list:
00566             s = fw_slash(s)
00567             d = src_re.sub(d_template, s)
00568             #print "s:",s, "d_t", d_template, "dst", dst, "d", d
00569             yield os_slash(s), os_slash(d)
00570 
00571     def path(self, src, dst=None):
00572         print "Processing", src, "=>", dst
00573         if src == None:
00574             raise RuntimeError("No source file, dst is " + dst)
00575         if dst == None:
00576             dst = src
00577         dst = os.path.join(self.get_dst_prefix(), dst)
00578         src = os.path.join(self.get_src_prefix(), src)
00579 
00580         # expand globs
00581         if(self.wildcard_pattern.search(src)):
00582             for s,d in self.expand_globs(src, dst):
00583                 self.process_file(s, d)
00584         else:
00585             # if we're specifying a single path (not a glob),
00586             # we should error out if it doesn't exist
00587             self.check_file_exists(src)
00588             # if it's a directory, recurse through it
00589             if(os.path.isdir(src)):
00590                 self.process_directory(src, dst)
00591             else:
00592                 self.process_file(src, dst)
00593 
00594 
00595     def do(self, *actions):
00596         self.actions = actions
00597         self.construct()
00598         # perform finish actions
00599         for action in self.actions:
00600             methodname = action + "_finish"
00601             method = getattr(self, methodname, None)
00602             if method is not None:
00603                 method()
00604         return self.file_list

Generated on Fri May 16 08:31:53 2008 for SecondLife by  doxygen 1.5.5