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
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
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
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
00184 args = {}
00185 for opt in options:
00186 args[opt[0].replace("--", "")] = opt[1]
00187
00188
00189 if args.has_key('help'):
00190
00191 usage(srctree)
00192 return
00193
00194
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
00204 if args.has_key('version') and type(args['version']) == str:
00205 args['version'] = args['version'].split('.')
00206
00207
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
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
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
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
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
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
00362 f = open(self.src_path_of(src), "rbU")
00363 contents = f.read()
00364 f.close()
00365
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
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
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
00399 tf = tarfile.open(self.src_path_of(unpacked_file_name), 'w:')
00400
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
00408 print "Cleaning up " + c
00409
00410 def process_file(self, src, dst):
00411 if(self.includes(src, dst)):
00412
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)
00462 os.symlink(linkto, dst)
00463 else:
00464
00465
00466
00467 if(os.path.exists(dst) and filecmp.cmp(src, dst, True)):
00468 return
00469
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
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
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
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
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
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
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
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
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
00586
00587 self.check_file_exists(src)
00588
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
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