PageRenderTime 116ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/indra/lib/python/indra/util/llmanifest.py

https://bitbucket.org/lindenlab/viewer-beta/
Python | 658 lines | 636 code | 5 blank | 17 comment | 21 complexity | ef3f58b95bc88822fa12997df2fba07a MD5 | raw file
Possible License(s): LGPL-2.1
  1. """\
  2. @file llmanifest.py
  3. @author Ryan Williams
  4. @brief Library for specifying operations on a set of files.
  5. $LicenseInfo:firstyear=2007&license=mit$
  6. Copyright (c) 2007-2009, Linden Research, Inc.
  7. Permission is hereby granted, free of charge, to any person obtaining a copy
  8. of this software and associated documentation files (the "Software"), to deal
  9. in the Software without restriction, including without limitation the rights
  10. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. copies of the Software, and to permit persons to whom the Software is
  12. furnished to do so, subject to the following conditions:
  13. The above copyright notice and this permission notice shall be included in
  14. all copies or substantial portions of the Software.
  15. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. THE SOFTWARE.
  22. $/LicenseInfo$
  23. """
  24. import commands
  25. import errno
  26. import filecmp
  27. import fnmatch
  28. import getopt
  29. import glob
  30. import os
  31. import re
  32. import shutil
  33. import sys
  34. import tarfile
  35. import errno
  36. import subprocess
  37. def path_ancestors(path):
  38. drive, path = os.path.splitdrive(os.path.normpath(path))
  39. result = []
  40. while len(path) > 0 and path != os.path.sep:
  41. result.append(drive+path)
  42. path, sub = os.path.split(path)
  43. return result
  44. def proper_windows_path(path, current_platform = sys.platform):
  45. """ This function takes an absolute Windows or Cygwin path and
  46. returns a path appropriately formatted for the platform it's
  47. running on (as determined by sys.platform)"""
  48. path = path.strip()
  49. drive_letter = None
  50. rel = None
  51. match = re.match("/cygdrive/([a-z])/(.*)", path)
  52. if not match:
  53. match = re.match('([a-zA-Z]):\\\(.*)', path)
  54. if not match:
  55. return None # not an absolute path
  56. drive_letter = match.group(1)
  57. rel = match.group(2)
  58. if current_platform == "cygwin":
  59. return "/cygdrive/" + drive_letter.lower() + '/' + rel.replace('\\', '/')
  60. else:
  61. return drive_letter.upper() + ':\\' + rel.replace('/', '\\')
  62. def get_default_platform(dummy):
  63. return {'linux2':'linux',
  64. 'linux1':'linux',
  65. 'cygwin':'windows',
  66. 'win32':'windows',
  67. 'darwin':'darwin'
  68. }[sys.platform]
  69. def get_default_version(srctree):
  70. # look up llversion.h and parse out the version info
  71. paths = [os.path.join(srctree, x, 'llversionviewer.h') for x in ['llcommon', '../llcommon', '../../indra/llcommon.h']]
  72. for p in paths:
  73. if os.path.exists(p):
  74. contents = open(p, 'r').read()
  75. major = re.search("LL_VERSION_MAJOR\s=\s([0-9]+)", contents).group(1)
  76. minor = re.search("LL_VERSION_MINOR\s=\s([0-9]+)", contents).group(1)
  77. patch = re.search("LL_VERSION_PATCH\s=\s([0-9]+)", contents).group(1)
  78. build = re.search("LL_VERSION_BUILD\s=\s([0-9]+)", contents).group(1)
  79. return major, minor, patch, build
  80. def get_channel(srctree):
  81. # look up llversionserver.h and parse out the version info
  82. paths = [os.path.join(srctree, x, 'llversionviewer.h') for x in ['llcommon', '../llcommon', '../../indra/llcommon.h']]
  83. for p in paths:
  84. if os.path.exists(p):
  85. contents = open(p, 'r').read()
  86. channel = re.search("LL_CHANNEL\s=\s\"(.+)\";\s*$", contents, flags = re.M).group(1)
  87. return channel
  88. DEFAULT_SRCTREE = os.path.dirname(sys.argv[0])
  89. DEFAULT_CHANNEL = 'Second Life Release'
  90. ARGUMENTS=[
  91. dict(name='actions',
  92. description="""This argument specifies the actions that are to be taken when the
  93. script is run. The meaningful actions are currently:
  94. copy - copies the files specified by the manifest into the
  95. destination directory.
  96. package - bundles up the files in the destination directory into
  97. an installer for the current platform
  98. unpacked - bundles up the files in the destination directory into
  99. a simple tarball
  100. Example use: %(name)s --actions="copy unpacked" """,
  101. default="copy package"),
  102. dict(name='arch',
  103. description="""This argument is appended to the platform string for
  104. determining which manifest class to run.
  105. Example use: %(name)s --arch=i686
  106. On Linux this would try to use Linux_i686Manifest.""",
  107. default=""),
  108. dict(name='build', description='Build directory.', default=DEFAULT_SRCTREE),
  109. dict(name='buildtype', description='Build type (i.e. Debug, Release, RelWithDebInfo).', default=None),
  110. dict(name='configuration',
  111. description="""The build configuration used.""",
  112. default="Release"),
  113. dict(name='dest', description='Destination directory.', default=DEFAULT_SRCTREE),
  114. dict(name='grid',
  115. description="""Which grid the client will try to connect to. Even
  116. though it's not strictly a grid, 'firstlook' is also an acceptable
  117. value for this parameter.""",
  118. default=""),
  119. dict(name='channel',
  120. description="""The channel to use for updates, packaging, settings name, etc.""",
  121. default=get_channel),
  122. dict(name='login_channel',
  123. description="""The channel to use for login handshake/updates only.""",
  124. default=None),
  125. dict(name='installer_name',
  126. description=""" The name of the file that the installer should be
  127. packaged up into. Only used on Linux at the moment.""",
  128. default=None),
  129. dict(name='login_url',
  130. description="""The url that the login screen displays in the client.""",
  131. default=None),
  132. dict(name='platform',
  133. description="""The current platform, to be used for looking up which
  134. manifest class to run.""",
  135. default=get_default_platform),
  136. dict(name='source',
  137. description='Source directory.',
  138. default=DEFAULT_SRCTREE),
  139. dict(name='artwork', description='Artwork directory.', default=DEFAULT_SRCTREE),
  140. dict(name='touch',
  141. description="""File to touch when action is finished. Touch file will
  142. contain the name of the final package in a form suitable
  143. for use by a .bat file.""",
  144. default=None),
  145. dict(name='version',
  146. description="""This specifies the version of Second Life that is
  147. being packaged up.""",
  148. default=get_default_version)
  149. ]
  150. def usage(srctree=""):
  151. nd = {'name':sys.argv[0]}
  152. print """Usage:
  153. %(name)s [options] [destdir]
  154. Options:
  155. """ % nd
  156. for arg in ARGUMENTS:
  157. default = arg['default']
  158. if hasattr(default, '__call__'):
  159. default = "(computed value) \"" + str(default(srctree)) + '"'
  160. elif default is not None:
  161. default = '"' + default + '"'
  162. print "\t--%s Default: %s\n\t%s\n" % (
  163. arg['name'],
  164. default,
  165. arg['description'] % nd)
  166. def main():
  167. option_names = [arg['name'] + '=' for arg in ARGUMENTS]
  168. option_names.append('help')
  169. options, remainder = getopt.getopt(sys.argv[1:], "", option_names)
  170. # convert options to a hash
  171. args = {'source': DEFAULT_SRCTREE,
  172. 'artwork': DEFAULT_SRCTREE,
  173. 'build': DEFAULT_SRCTREE,
  174. 'dest': DEFAULT_SRCTREE }
  175. for opt in options:
  176. args[opt[0].replace("--", "")] = opt[1]
  177. for k in 'artwork build dest source'.split():
  178. args[k] = os.path.normpath(args[k])
  179. print "Source tree:", args['source']
  180. print "Artwork tree:", args['artwork']
  181. print "Build tree:", args['build']
  182. print "Destination tree:", args['dest']
  183. # early out for help
  184. if 'help' in args:
  185. # *TODO: it is a huge hack to pass around the srctree like this
  186. usage(args['source'])
  187. return
  188. # defaults
  189. for arg in ARGUMENTS:
  190. if arg['name'] not in args:
  191. default = arg['default']
  192. if hasattr(default, '__call__'):
  193. default = default(args['source'])
  194. if default is not None:
  195. args[arg['name']] = default
  196. # fix up version
  197. if isinstance(args.get('version'), str):
  198. args['version'] = args['version'].split('.')
  199. # default and agni are default
  200. if args['grid'] in ['default', 'agni']:
  201. args['grid'] = ''
  202. if 'actions' in args:
  203. args['actions'] = args['actions'].split()
  204. # debugging
  205. for opt in args:
  206. print "Option:", opt, "=", args[opt]
  207. wm = LLManifest.for_platform(args['platform'], args.get('arch'))(args)
  208. wm.do(*args['actions'])
  209. # Write out the package file in this format, so that it can easily be called
  210. # and used in a .bat file - yeah, it sucks, but this is the simplest...
  211. touch = args.get('touch')
  212. if touch:
  213. fp = open(touch, 'w')
  214. fp.write('set package_file=%s\n' % wm.package_file)
  215. fp.close()
  216. print 'touched', touch
  217. return 0
  218. class LLManifestRegistry(type):
  219. def __init__(cls, name, bases, dct):
  220. super(LLManifestRegistry, cls).__init__(name, bases, dct)
  221. match = re.match("(\w+)Manifest", name)
  222. if match:
  223. cls.manifests[match.group(1).lower()] = cls
  224. class LLManifest(object):
  225. __metaclass__ = LLManifestRegistry
  226. manifests = {}
  227. def for_platform(self, platform, arch = None):
  228. if arch:
  229. platform = platform + '_' + arch
  230. return self.manifests[platform.lower()]
  231. for_platform = classmethod(for_platform)
  232. def __init__(self, args):
  233. super(LLManifest, self).__init__()
  234. self.args = args
  235. self.file_list = []
  236. self.excludes = []
  237. self.actions = []
  238. self.src_prefix = [args['source']]
  239. self.artwork_prefix = [args['artwork']]
  240. self.build_prefix = [args['build']]
  241. self.dst_prefix = [args['dest']]
  242. self.created_paths = []
  243. self.package_name = "Unknown"
  244. def default_grid(self):
  245. return self.args.get('grid', None) == ''
  246. def default_channel(self):
  247. return self.args.get('channel', None) == DEFAULT_CHANNEL
  248. def construct(self):
  249. """ Meant to be overriden by LLManifest implementors with code that
  250. constructs the complete destination hierarchy."""
  251. pass # override this method
  252. def exclude(self, glob):
  253. """ Excludes all files that match the glob from being included
  254. in the file list by path()."""
  255. self.excludes.append(glob)
  256. def prefix(self, src='', build=None, dst=None):
  257. """ Pushes a prefix onto the stack. Until end_prefix is
  258. called, all relevant method calls (esp. to path()) will prefix
  259. paths with the entire prefix stack. Source and destination
  260. prefixes can be different, though if only one is provided they
  261. are both equal. To specify a no-op, use an empty string, not
  262. None."""
  263. if dst is None:
  264. dst = src
  265. if build is None:
  266. build = src
  267. self.src_prefix.append(src)
  268. self.artwork_prefix.append(src)
  269. self.build_prefix.append(build)
  270. self.dst_prefix.append(dst)
  271. return True # so that you can wrap it in an if to get indentation
  272. def end_prefix(self, descr=None):
  273. """Pops a prefix off the stack. If given an argument, checks
  274. the argument against the top of the stack. If the argument
  275. matches neither the source or destination prefixes at the top
  276. of the stack, then misnesting must have occurred and an
  277. exception is raised."""
  278. # 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.
  279. src = self.src_prefix.pop()
  280. artwork = self.artwork_prefix.pop()
  281. build = self.build_prefix.pop()
  282. dst = self.dst_prefix.pop()
  283. if descr and not(src == descr or build == descr or dst == descr):
  284. raise ValueError, "End prefix '" + descr + "' didn't match '" +src+ "' or '" +dst + "'"
  285. def get_src_prefix(self):
  286. """ Returns the current source prefix."""
  287. return os.path.join(*self.src_prefix)
  288. def get_artwork_prefix(self):
  289. """ Returns the current artwork prefix."""
  290. return os.path.join(*self.artwork_prefix)
  291. def get_build_prefix(self):
  292. """ Returns the current build prefix."""
  293. return os.path.join(*self.build_prefix)
  294. def get_dst_prefix(self):
  295. """ Returns the current destination prefix."""
  296. return os.path.join(*self.dst_prefix)
  297. def src_path_of(self, relpath):
  298. """Returns the full path to a file or directory specified
  299. relative to the source directory."""
  300. return os.path.join(self.get_src_prefix(), relpath)
  301. def build_path_of(self, relpath):
  302. """Returns the full path to a file or directory specified
  303. relative to the build directory."""
  304. return os.path.join(self.get_build_prefix(), relpath)
  305. def dst_path_of(self, relpath):
  306. """Returns the full path to a file or directory specified
  307. relative to the destination directory."""
  308. return os.path.join(self.get_dst_prefix(), relpath)
  309. def ensure_src_dir(self, reldir):
  310. """Construct the path for a directory relative to the
  311. source path, and ensures that it exists. Returns the
  312. full path."""
  313. path = os.path.join(self.get_src_prefix(), reldir)
  314. self.cmakedirs(path)
  315. return path
  316. def ensure_dst_dir(self, reldir):
  317. """Construct the path for a directory relative to the
  318. destination path, and ensures that it exists. Returns the
  319. full path."""
  320. path = os.path.join(self.get_dst_prefix(), reldir)
  321. self.cmakedirs(path)
  322. return path
  323. def run_command(self, command):
  324. """ Runs an external command, and returns the output. Raises
  325. an exception if the command returns a nonzero status code. For
  326. debugging/informational purposes, prints out the command's
  327. output as it is received."""
  328. print "Running command:", command
  329. sys.stdout.flush()
  330. child = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  331. shell=True)
  332. lines = []
  333. while True:
  334. lines.append(child.stdout.readline())
  335. if lines[-1] == '':
  336. break
  337. else:
  338. print lines[-1],
  339. output = ''.join(lines)
  340. child.stdout.close()
  341. status = child.wait()
  342. if status:
  343. raise RuntimeError(
  344. "Command %s returned non-zero status (%s) \noutput:\n%s"
  345. % (command, status, output) )
  346. return output
  347. def created_path(self, path):
  348. """ Declare that you've created a path in order to
  349. a) verify that you really have created it
  350. b) schedule it for cleanup"""
  351. if not os.path.exists(path):
  352. raise RuntimeError, "Should be something at path " + path
  353. self.created_paths.append(path)
  354. def put_in_file(self, contents, dst):
  355. # write contents as dst
  356. f = open(self.dst_path_of(dst), "wb")
  357. f.write(contents)
  358. f.close()
  359. def replace_in(self, src, dst=None, searchdict={}):
  360. if dst == None:
  361. dst = src
  362. # read src
  363. f = open(self.src_path_of(src), "rbU")
  364. contents = f.read()
  365. f.close()
  366. # apply dict replacements
  367. for old, new in searchdict.iteritems():
  368. contents = contents.replace(old, new)
  369. self.put_in_file(contents, dst)
  370. self.created_paths.append(dst)
  371. def copy_action(self, src, dst):
  372. if src and (os.path.exists(src) or os.path.islink(src)):
  373. # ensure that destination path exists
  374. self.cmakedirs(os.path.dirname(dst))
  375. self.created_paths.append(dst)
  376. if not os.path.isdir(src):
  377. self.ccopy(src,dst)
  378. else:
  379. # src is a dir
  380. self.ccopytree(src,dst)
  381. else:
  382. print "Doesn't exist:", src
  383. def package_action(self, src, dst):
  384. pass
  385. def copy_finish(self):
  386. pass
  387. def package_finish(self):
  388. pass
  389. def unpacked_finish(self):
  390. unpacked_file_name = "unpacked_%(plat)s_%(vers)s.tar" % {
  391. 'plat':self.args['platform'],
  392. 'vers':'_'.join(self.args['version'])}
  393. print "Creating unpacked file:", unpacked_file_name
  394. # could add a gz here but that doubles the time it takes to do this step
  395. tf = tarfile.open(self.src_path_of(unpacked_file_name), 'w:')
  396. # add the entire installation package, at the very top level
  397. tf.add(self.get_dst_prefix(), "")
  398. tf.close()
  399. def cleanup_finish(self):
  400. """ Delete paths that were specified to have been created by this script"""
  401. for c in self.created_paths:
  402. # *TODO is this gonna be useful?
  403. print "Cleaning up " + c
  404. def process_file(self, src, dst):
  405. if self.includes(src, dst):
  406. # print src, "=>", dst
  407. for action in self.actions:
  408. methodname = action + "_action"
  409. method = getattr(self, methodname, None)
  410. if method is not None:
  411. method(src, dst)
  412. self.file_list.append([src, dst])
  413. return 1
  414. else:
  415. sys.stdout.write(" (excluding %r, %r)" % (src, dst))
  416. sys.stdout.flush()
  417. return 0
  418. def process_directory(self, src, dst):
  419. if not self.includes(src, dst):
  420. sys.stdout.write(" (excluding %r, %r)" % (src, dst))
  421. sys.stdout.flush()
  422. return 0
  423. names = os.listdir(src)
  424. self.cmakedirs(dst)
  425. errors = []
  426. count = 0
  427. for name in names:
  428. srcname = os.path.join(src, name)
  429. dstname = os.path.join(dst, name)
  430. if os.path.isdir(srcname):
  431. count += self.process_directory(srcname, dstname)
  432. else:
  433. count += self.process_file(srcname, dstname)
  434. return count
  435. def includes(self, src, dst):
  436. if src:
  437. for excl in self.excludes:
  438. if fnmatch.fnmatch(src, excl):
  439. return False
  440. return True
  441. def remove(self, *paths):
  442. for path in paths:
  443. if os.path.exists(path):
  444. print "Removing path", path
  445. if os.path.isdir(path):
  446. shutil.rmtree(path)
  447. else:
  448. os.remove(path)
  449. def ccopy(self, src, dst):
  450. """ Copy a single file or symlink. Uses filecmp to skip copying for existing files."""
  451. if os.path.islink(src):
  452. linkto = os.readlink(src)
  453. if os.path.islink(dst) or os.path.exists(dst):
  454. os.remove(dst) # because symlinking over an existing link fails
  455. os.symlink(linkto, dst)
  456. else:
  457. # Don't recopy file if it's up-to-date.
  458. # If we seem to be not not overwriting files that have been
  459. # updated, set the last arg to False, but it will take longer.
  460. if os.path.exists(dst) and filecmp.cmp(src, dst, True):
  461. return
  462. # only copy if it's not excluded
  463. if self.includes(src, dst):
  464. try:
  465. os.unlink(dst)
  466. except OSError, err:
  467. if err.errno != errno.ENOENT:
  468. raise
  469. shutil.copy2(src, dst)
  470. def ccopytree(self, src, dst):
  471. """Direct copy of shutil.copytree with the additional
  472. feature that the destination directory can exist. It
  473. is so dumb that Python doesn't come with this. Also it
  474. implements the excludes functionality."""
  475. if not self.includes(src, dst):
  476. return
  477. names = os.listdir(src)
  478. self.cmakedirs(dst)
  479. errors = []
  480. for name in names:
  481. srcname = os.path.join(src, name)
  482. dstname = os.path.join(dst, name)
  483. try:
  484. if os.path.isdir(srcname):
  485. self.ccopytree(srcname, dstname)
  486. else:
  487. self.ccopy(srcname, dstname)
  488. # XXX What about devices, sockets etc.?
  489. except (IOError, os.error), why:
  490. errors.append((srcname, dstname, why))
  491. if errors:
  492. raise RuntimeError, errors
  493. def cmakedirs(self, path):
  494. """Ensures that a directory exists, and doesn't throw an exception
  495. if you call it on an existing directory."""
  496. # print "making path: ", path
  497. path = os.path.normpath(path)
  498. self.created_paths.append(path)
  499. if not os.path.exists(path):
  500. os.makedirs(path)
  501. def find_existing_file(self, *list):
  502. for f in list:
  503. if os.path.exists(f):
  504. return f
  505. # didn't find it, return last item in list
  506. if len(list) > 0:
  507. return list[-1]
  508. else:
  509. return None
  510. def contents_of_tar(self, src_tar, dst_dir):
  511. """ Extracts the contents of the tarfile (specified
  512. relative to the source prefix) into the directory
  513. specified relative to the destination directory."""
  514. self.check_file_exists(src_tar)
  515. tf = tarfile.open(self.src_path_of(src_tar), 'r')
  516. for member in tf.getmembers():
  517. tf.extract(member, self.ensure_dst_dir(dst_dir))
  518. # TODO get actions working on these dudes, perhaps we should extract to a temporary directory and then process_directory on it?
  519. self.file_list.append([src_tar,
  520. self.dst_path_of(os.path.join(dst_dir,member.name))])
  521. tf.close()
  522. def wildcard_regex(self, src_glob, dst_glob):
  523. src_re = re.escape(src_glob)
  524. src_re = src_re.replace('\*', '([-a-zA-Z0-9._ ]*)')
  525. dst_temp = dst_glob
  526. i = 1
  527. while dst_temp.count("*") > 0:
  528. dst_temp = dst_temp.replace('*', '\g<' + str(i) + '>', 1)
  529. i = i+1
  530. return re.compile(src_re), dst_temp
  531. def check_file_exists(self, path):
  532. if not os.path.exists(path) and not os.path.islink(path):
  533. raise RuntimeError("Path %s doesn't exist" % (
  534. os.path.normpath(os.path.join(os.getcwd(), path)),))
  535. wildcard_pattern = re.compile('\*')
  536. def expand_globs(self, src, dst):
  537. src_list = glob.glob(src)
  538. src_re, d_template = self.wildcard_regex(src.replace('\\', '/'),
  539. dst.replace('\\', '/'))
  540. for s in src_list:
  541. d = src_re.sub(d_template, s.replace('\\', '/'))
  542. yield os.path.normpath(s), os.path.normpath(d)
  543. def path(self, src, dst=None):
  544. sys.stdout.write("Processing %s => %s ... " % (src, dst))
  545. sys.stdout.flush()
  546. if src == None:
  547. raise RuntimeError("No source file, dst is " + dst)
  548. if dst == None:
  549. dst = src
  550. dst = os.path.join(self.get_dst_prefix(), dst)
  551. def try_path(src):
  552. # expand globs
  553. count = 0
  554. if self.wildcard_pattern.search(src):
  555. for s,d in self.expand_globs(src, dst):
  556. assert(s != d)
  557. count += self.process_file(s, d)
  558. else:
  559. # if we're specifying a single path (not a glob),
  560. # we should error out if it doesn't exist
  561. self.check_file_exists(src)
  562. # if it's a directory, recurse through it
  563. if os.path.isdir(src):
  564. count += self.process_directory(src, dst)
  565. else:
  566. count += self.process_file(src, dst)
  567. return count
  568. try:
  569. count = try_path(os.path.join(self.get_src_prefix(), src))
  570. except RuntimeError:
  571. try:
  572. count = try_path(os.path.join(self.get_artwork_prefix(), src))
  573. except RuntimeError:
  574. count = try_path(os.path.join(self.get_build_prefix(), src))
  575. print "%d files" % count
  576. def do(self, *actions):
  577. self.actions = actions
  578. self.construct()
  579. # perform finish actions
  580. for action in self.actions:
  581. methodname = action + "_finish"
  582. method = getattr(self, methodname, None)
  583. if method is not None:
  584. method()
  585. return self.file_list