llmanifest.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. """\
  2. @file llmanifest.py
  3. @author Ryan Williams
  4. @brief Python2 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. # NOTE: could not be converted to run under both Python 2 and Python 3 because
  25. # of the __metaclass__ usage (would need the six library dependency).
  26. # There are therefore llmanifest.py and llmanifest3.py, and the consumer script
  27. # (viewer_manifest.py) imports either of the two, depending on the Python
  28. # version it runs under...
  29. import sys
  30. import os
  31. import errno
  32. import filecmp
  33. import fnmatch
  34. import getopt
  35. import glob
  36. import re
  37. import shutil
  38. import tarfile
  39. import errno
  40. class ManifestError(RuntimeError):
  41. """Use an exception more specific than generic Python RuntimeError"""
  42. pass
  43. class MissingError(ManifestError):
  44. """You specified a file that doesn't exist"""
  45. pass
  46. def path_ancestors(path):
  47. drive, path = os.path.splitdrive(os.path.normpath(path))
  48. result = []
  49. while len(path) > 0 and path != os.path.sep:
  50. result.append(drive+path)
  51. path, sub = os.path.split(path)
  52. return result
  53. def proper_windows_path(path, current_platform = sys.platform):
  54. """ This function takes an absolute Windows or Cygwin path and
  55. returns a path appropriately formatted for the platform it's
  56. running on (as determined by sys.platform)"""
  57. path = path.strip()
  58. drive_letter = None
  59. rel = None
  60. match = re.match("/cygdrive/([a-z])/(.*)", path)
  61. if not match:
  62. match = re.match('([a-zA-Z]):\\\(.*)', path)
  63. if not match:
  64. return None # not an absolute path
  65. drive_letter = match.group(1)
  66. rel = match.group(2)
  67. if current_platform == "cygwin":
  68. return "/cygdrive/" + drive_letter.lower() + '/' + rel.replace('\\', '/')
  69. else:
  70. return drive_letter.upper() + ':\\' + rel.replace('/', '\\')
  71. def get_default_platform(dummy):
  72. return {'linux':'linux',
  73. 'linux1':'linux',
  74. 'linux2':'linux',
  75. 'cygwin':'windows',
  76. 'win32':'windows',
  77. 'darwin':'darwin',
  78. 'darwin64':'darwin',
  79. }[sys.platform]
  80. def get_default_version(srctree):
  81. # look up llversion.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. major = re.search("LL_VERSION_MAJOR\s=\s([0-9]+)", contents).group(1)
  87. minor = re.search("LL_VERSION_MINOR\s=\s([0-9]+)", contents).group(1)
  88. patch = re.search("LL_VERSION_BRANCH\s=\s([0-9]+)", contents).group(1)
  89. build = re.search("LL_VERSION_RELEASE\s=\s([0-9]+)", contents).group(1)
  90. return major, minor, patch, build
  91. DEFAULT_SRCTREE = os.path.dirname(sys.argv[0])
  92. ARGUMENTS=[
  93. dict(name='actions',
  94. description="""This argument specifies the actions that are to be taken when the
  95. script is run. The meaningful actions are currently:
  96. copy - copies the files specified by the manifest into the
  97. destination directory.
  98. package - bundles up the files in the destination directory into
  99. an installer for the current platform
  100. unpacked - bundles up the files in the destination directory into
  101. a simple tarball
  102. Example use: %(name)s --actions="copy unpacked" """,
  103. default="copy package"),
  104. dict(name='build', description='Build directory.', default=DEFAULT_SRCTREE),
  105. dict(name='buildtype', description="""The build type used. ('Debug', 'Release', or 'RelWithDebInfo')
  106. Default is Release """,
  107. default="Release"),
  108. dict(name='branding_id', description="""Identifier for the branding set to
  109. use. Currently, 'cool_vl_viewer')""",
  110. default='secondlife'),
  111. dict(name='configuration',
  112. description="""The build configuration used.""", default="Universal"),
  113. dict(name='dest', description='Destination directory.', default=DEFAULT_SRCTREE),
  114. dict(name='installer_name',
  115. description=""" The name of the file that the installer should be
  116. packaged up into. Only used on Linux for the tar file name at the moment.""",
  117. default=None),
  118. dict(name='platform',
  119. description="""The current platform, to be used for looking up which
  120. manifest class to run.""",
  121. default=get_default_platform),
  122. dict(name='source',
  123. description='Source directory.',
  124. default=DEFAULT_SRCTREE),
  125. dict(name='touch',
  126. description="""File to touch when action is finished. Touch file will
  127. contain the name of the final package in a form suitable
  128. for use by a .bat file.""",
  129. default=None),
  130. dict(name='version',
  131. description="""This specifies the version of the viewer that is
  132. being packaged up.""",
  133. default=get_default_version),
  134. dict(name='custom',
  135. description='Used to pass custom options to the viewer manifest script',
  136. default=None)
  137. ]
  138. def usage(srctree=""):
  139. nd = {'name':sys.argv[0]}
  140. print """Usage:
  141. %(name)s [options] [destdir]
  142. Options:
  143. """ % nd
  144. for arg in ARGUMENTS:
  145. default = arg['default']
  146. if hasattr(default, '__call__'):
  147. default = "(computed value) \"" + str(default(srctree)) + '"'
  148. elif default is not None:
  149. default = '"' + default + '"'
  150. print "\t--%s Default: %s\n\t%s\n" % (
  151. arg['name'],
  152. default,
  153. arg['description'] % nd)
  154. def main():
  155. option_names = [arg['name'] + '=' for arg in ARGUMENTS]
  156. option_names.append('help')
  157. options, remainder = getopt.getopt(sys.argv[1:], "", option_names)
  158. # convert options to a hash
  159. args = {'source': DEFAULT_SRCTREE,
  160. 'build': DEFAULT_SRCTREE,
  161. 'dest': DEFAULT_SRCTREE }
  162. for opt in options:
  163. args[opt[0].replace("--", "")] = opt[1]
  164. for k in 'build dest source'.split():
  165. args[k] = os.path.normpath(args[k])
  166. print "Source tree:", args['source']
  167. print "Build tree:", args['build']
  168. print "Destination tree:", args['dest']
  169. # early out for help
  170. if 'help' in args:
  171. # *TODO: it is a huge hack to pass around the srctree like this
  172. usage(args['source'])
  173. return
  174. # defaults
  175. for arg in ARGUMENTS:
  176. if arg['name'] not in args:
  177. default = arg['default']
  178. if hasattr(default, '__call__'):
  179. default = default(args['source'])
  180. if default is not None:
  181. args[arg['name']] = default
  182. # fix up version
  183. if isinstance(args.get('version'), str):
  184. args['version'] = args['version'].split('.')
  185. if 'actions' in args:
  186. args['actions'] = args['actions'].split()
  187. # debugging
  188. for opt in args:
  189. print "Option:", opt, "=", args[opt]
  190. wm = LLManifest.for_platform(args['platform'])(args)
  191. wm.do(*args['actions'])
  192. # Write out the package file in this format, so that it can easily be called
  193. # and used in a .bat file - yeah, it sucks, but this is the simplest...
  194. touch = args.get('touch')
  195. if touch:
  196. fp = open(touch, 'w')
  197. fp.write('set package_file=%s\n' % wm.package_file)
  198. fp.close()
  199. print 'touched', touch
  200. return 0
  201. class LLManifestRegistry(type):
  202. def __init__(cls, name, bases, dct):
  203. super(LLManifestRegistry, cls).__init__(name, bases, dct)
  204. match = re.match("(\w+)Manifest", name)
  205. if match:
  206. cls.manifests[match.group(1).lower()] = cls
  207. class LLManifest(object):
  208. __metaclass__ = LLManifestRegistry
  209. manifests = {}
  210. def for_platform(self, platform):
  211. return self.manifests[platform.lower()]
  212. for_platform = classmethod(for_platform)
  213. def __init__(self, args):
  214. super(LLManifest, self).__init__()
  215. self.args = args
  216. self.file_list = []
  217. self.excludes = []
  218. self.actions = []
  219. self.src_prefix = [args['source']]
  220. self.build_prefix = [args['build']]
  221. self.dst_prefix = [args['dest']]
  222. self.created_paths = []
  223. self.package_name = "Unknown"
  224. def construct(self):
  225. """ Meant to be overriden by LLManifest implementors with code that
  226. constructs the complete destination hierarchy."""
  227. pass # override this method
  228. def exclude(self, glob):
  229. """ Excludes all files that match the glob from being included
  230. in the file list by path()."""
  231. self.excludes.append(glob)
  232. def prefix(self, src='', build=None, dst=None):
  233. """ Pushes a prefix onto the stack. Until end_prefix is
  234. called, all relevant method calls (esp. to path()) will prefix
  235. paths with the entire prefix stack. Source and destination
  236. prefixes can be different, though if only one is provided they
  237. are both equal. To specify a no-op, use an empty string, not
  238. None."""
  239. if dst is None:
  240. dst = src
  241. if build is None:
  242. build = src
  243. self.src_prefix.append(src)
  244. self.build_prefix.append(build)
  245. self.dst_prefix.append(dst)
  246. return True # so that you can wrap it in an if to get indentation
  247. def end_prefix(self, descr=None):
  248. """Pops a prefix off the stack. If given an argument, checks
  249. the argument against the top of the stack. If the argument
  250. matches neither the source or destination prefixes at the top
  251. of the stack, then misnesting must have occurred and an
  252. exception is raised."""
  253. # 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.
  254. src = self.src_prefix.pop()
  255. build = self.build_prefix.pop()
  256. dst = self.dst_prefix.pop()
  257. if descr and not(src == descr or build == descr or dst == descr):
  258. raise ValueError, "End prefix '" + descr + "' didn't match '" +src+ "' or '" +dst + "'"
  259. def get_src_prefix(self):
  260. """ Returns the current source prefix."""
  261. return os.path.join(*self.src_prefix)
  262. def get_build_prefix(self):
  263. """ Returns the current build prefix."""
  264. return os.path.join(*self.build_prefix)
  265. def get_dst_prefix(self):
  266. """ Returns the current destination prefix."""
  267. return os.path.join(*self.dst_prefix)
  268. def src_path_of(self, relpath):
  269. """Returns the full path to a file or directory specified
  270. relative to the source directory."""
  271. return os.path.join(self.get_src_prefix(), relpath)
  272. def build_path_of(self, relpath):
  273. """Returns the full path to a file or directory specified
  274. relative to the build directory."""
  275. return os.path.join(self.get_build_prefix(), relpath)
  276. def dst_path_of(self, relpath):
  277. """Returns the full path to a file or directory specified
  278. relative to the destination directory."""
  279. return os.path.join(self.get_dst_prefix(), relpath)
  280. def ensure_src_dir(self, reldir):
  281. """Construct the path for a directory relative to the
  282. source path, and ensures that it exists. Returns the
  283. full path."""
  284. path = os.path.join(self.get_src_prefix(), reldir)
  285. self.cmakedirs(path)
  286. return path
  287. def ensure_dst_dir(self, reldir):
  288. """Construct the path for a directory relative to the
  289. destination path, and ensures that it exists. Returns the
  290. full path."""
  291. path = os.path.join(self.get_dst_prefix(), reldir)
  292. self.cmakedirs(path)
  293. return path
  294. def run_command(self, command):
  295. """ Runs an external command, and returns the output. Raises
  296. an exception if the command reurns a nonzero status code. For
  297. debugging/informational purpoases, prints out the command's
  298. output as it is received."""
  299. print "Running command:", command
  300. fd = os.popen(command, 'r')
  301. lines = []
  302. while True:
  303. lines.append(fd.readline())
  304. if lines[-1] == '':
  305. break
  306. else:
  307. print lines[-1],
  308. output = ''.join(lines)
  309. status = fd.close()
  310. if status:
  311. raise RuntimeError(
  312. "Command %s returned non-zero status (%s) \noutput:\n%s"
  313. % (command, status, output) )
  314. return output
  315. def created_path(self, path):
  316. """ Declare that you've created a path in order to
  317. a) verify that you really have created it
  318. b) schedule it for cleanup"""
  319. if not os.path.exists(path):
  320. raise RuntimeError, "Should be something at path " + path
  321. self.created_paths.append(path)
  322. def put_in_file(self, contents, dst, src=None):
  323. # write contents as dst
  324. dst_path = self.dst_path_of(dst)
  325. f = open(dst_path, "wb")
  326. try:
  327. f.write(contents)
  328. finally:
  329. f.close()
  330. # Why would we create a file in the destination tree if not to include
  331. # it in the installer? The default src=None (plus the fact that the
  332. # src param is last) is to preserve backwards compatibility.
  333. if src:
  334. self.file_list.append([src, dst_path])
  335. return dst_path
  336. def replace_in(self, src, dst=None, searchdict={}):
  337. if dst == None:
  338. dst = src
  339. # read src
  340. f = open(self.src_path_of(src), "rbU")
  341. contents = f.read()
  342. f.close()
  343. # apply dict replacements
  344. for old, new in searchdict.iteritems():
  345. contents = contents.replace(old, new)
  346. self.put_in_file(contents, dst)
  347. self.created_paths.append(dst)
  348. def copy_action(self, src, dst):
  349. if src and (os.path.exists(src) or os.path.islink(src)):
  350. # ensure that destination path exists
  351. self.cmakedirs(os.path.dirname(dst))
  352. self.created_paths.append(dst)
  353. if not os.path.isdir(src):
  354. self.ccopy(src,dst)
  355. else:
  356. # src is a dir
  357. self.ccopytree(src,dst)
  358. else:
  359. print "Doesn't exist:", src
  360. def package_action(self, src, dst):
  361. pass
  362. def copy_finish(self):
  363. pass
  364. def package_finish(self):
  365. pass
  366. def unpacked_finish(self):
  367. unpacked_file_name = "unpacked_%(plat)s_%(vers)s.tar" % {
  368. 'plat':self.args['platform'],
  369. 'vers':'_'.join(self.args['version'])}
  370. print "Creating unpacked file:", unpacked_file_name
  371. # could add a gz here but that doubles the time it takes to do this step
  372. tf = tarfile.open(self.src_path_of(unpacked_file_name), 'w:')
  373. # add the entire installation package, at the very top level
  374. tf.add(self.get_dst_prefix(), "")
  375. tf.close()
  376. def cleanup_finish(self):
  377. """ Delete paths that were specified to have been created by this script"""
  378. for c in self.created_paths:
  379. # *TODO is this gonna be useful?
  380. print "Cleaning up " + c
  381. def process_file(self, src, dst):
  382. if self.includes(src, dst):
  383. # print src, "=>", dst
  384. for action in self.actions:
  385. methodname = action + "_action"
  386. method = getattr(self, methodname, None)
  387. if method is not None:
  388. method(src, dst)
  389. self.file_list.append([src, dst])
  390. return 1
  391. else:
  392. sys.stdout.write(" (excluding %r, %r)" % (src, dst))
  393. sys.stdout.flush()
  394. return 0
  395. def process_directory(self, src, dst):
  396. if not self.includes(src, dst):
  397. sys.stdout.write(" (excluding %r, %r)" % (src, dst))
  398. sys.stdout.flush()
  399. return 0
  400. names = os.listdir(src)
  401. self.cmakedirs(dst)
  402. errors = []
  403. count = 0
  404. for name in names:
  405. srcname = os.path.join(src, name)
  406. dstname = os.path.join(dst, name)
  407. if os.path.isdir(srcname):
  408. count += self.process_directory(srcname, dstname)
  409. else:
  410. count += self.process_file(srcname, dstname)
  411. return count
  412. def includes(self, src, dst):
  413. if src:
  414. for excl in self.excludes:
  415. if fnmatch.fnmatch(src, excl):
  416. return False
  417. return True
  418. def remove(self, *paths):
  419. for path in paths:
  420. if os.path.exists(path):
  421. print "Removing path", path
  422. if os.path.isdir(path):
  423. shutil.rmtree(path)
  424. else:
  425. os.remove(path)
  426. def ccopy(self, src, dst):
  427. """ Copy a single file or symlink. Uses filecmp to skip copying for existing files."""
  428. if os.path.islink(src):
  429. linkto = os.readlink(src)
  430. if os.path.islink(dst) or os.path.exists(dst):
  431. os.remove(dst) # because symlinking over an existing link fails
  432. os.symlink(linkto, dst)
  433. else:
  434. # Don't recopy file if it's up-to-date.
  435. # If we seem to be not not overwriting files that have been
  436. # updated, set the last arg to False, but it will take longer.
  437. if os.path.exists(dst) and filecmp.cmp(src, dst, True):
  438. return
  439. # only copy if it's not excluded
  440. if self.includes(src, dst):
  441. try:
  442. os.unlink(dst)
  443. except OSError, err:
  444. if err.errno != errno.ENOENT:
  445. raise
  446. shutil.copy2(src, dst)
  447. def ccopytree(self, src, dst):
  448. """Direct copy of shutil.copytree with the additional
  449. feature that the destination directory can exist. It
  450. is so dumb that Python doesn't come with this. Also it
  451. implements the excludes functionality."""
  452. if not self.includes(src, dst):
  453. return
  454. names = os.listdir(src)
  455. self.cmakedirs(dst)
  456. errors = []
  457. for name in names:
  458. srcname = os.path.join(src, name)
  459. dstname = os.path.join(dst, name)
  460. try:
  461. if os.path.isdir(srcname):
  462. self.ccopytree(srcname, dstname)
  463. else:
  464. self.ccopy(srcname, dstname)
  465. # XXX What about devices, sockets etc.?
  466. except (IOError, os.error), why:
  467. errors.append((srcname, dstname, why))
  468. if errors:
  469. raise RuntimeError, errors
  470. def cmakedirs(self, path):
  471. """Ensures that a directory exists, and doesn't throw an exception
  472. if you call it on an existing directory."""
  473. # print "making path: ", path
  474. path = os.path.normpath(path)
  475. self.created_paths.append(path)
  476. if not os.path.exists(path):
  477. os.makedirs(path)
  478. def find_existing_file(self, *list):
  479. for f in list:
  480. if os.path.exists(f):
  481. return f
  482. # didn't find it, return last item in list
  483. if len(list) > 0:
  484. return list[-1]
  485. else:
  486. return None
  487. def contents_of_tar(self, src_tar, dst_dir):
  488. """ Extracts the contents of the tarfile (specified
  489. relative to the source prefix) into the directory
  490. specified relative to the destination directory."""
  491. self.check_file_exists(src_tar)
  492. tf = tarfile.open(self.src_path_of(src_tar), 'r')
  493. for member in tf.getmembers():
  494. tf.extract(member, self.ensure_dst_dir(dst_dir))
  495. # TODO get actions working on these dudes, perhaps we should extract to a temporary directory and then process_directory on it?
  496. self.file_list.append([src_tar,
  497. self.dst_path_of(os.path.join(dst_dir,member.name))])
  498. tf.close()
  499. def wildcard_regex(self, src_glob, dst_glob):
  500. src_re = re.escape(src_glob)
  501. src_re = src_re.replace('\*', '([-a-zA-Z0-9._ ]*)')
  502. dst_temp = dst_glob
  503. i = 1
  504. while dst_temp.count("*") > 0:
  505. dst_temp = dst_temp.replace('*', '\g<' + str(i) + '>', 1)
  506. i = i+1
  507. return re.compile(src_re), dst_temp
  508. def check_file_exists(self, path):
  509. if not os.path.exists(path) and not os.path.islink(path):
  510. raise MissingError("Path %s doesn't exist" % (os.path.abspath(path),))
  511. wildcard_pattern = re.compile('\*')
  512. def expand_globs(self, src, dst):
  513. src_list = glob.glob(src)
  514. src_re, d_template = self.wildcard_regex(src.replace('\\', '/'),
  515. dst.replace('\\', '/'))
  516. for s in src_list:
  517. d = src_re.sub(d_template, s.replace('\\', '/'))
  518. yield os.path.normpath(s), os.path.normpath(d)
  519. def path2basename(self, path, file):
  520. """
  521. It is a common idiom to write:
  522. self.path(os.path.join(somedir, somefile), somefile)
  523. So instead you can write:
  524. self.path2basename(somedir, somefile)
  525. Note that this is NOT the same as:
  526. self.path(os.path.join(somedir, somefile))
  527. which is the same as:
  528. temppath = os.path.join(somedir, somefile)
  529. self.path(temppath, temppath)
  530. """
  531. return self.path(os.path.join(path, file), file)
  532. def path(self, src, dst=None):
  533. sys.stdout.write("Processing %s => %s ... " % (src, dst))
  534. sys.stdout.flush()
  535. if src == None:
  536. raise ManifestError("No source file, dst is " + dst)
  537. if dst == None:
  538. dst = src
  539. dst = os.path.join(self.get_dst_prefix(), dst)
  540. def try_path(src):
  541. # expand globs
  542. count = 0
  543. if self.wildcard_pattern.search(src):
  544. for s,d in self.expand_globs(src, dst):
  545. assert(s != d)
  546. count += self.process_file(s, d)
  547. else:
  548. # if we're specifying a single path (not a glob),
  549. # we should error out if it doesn't exist
  550. self.check_file_exists(src)
  551. # if it's a directory, recurse through it
  552. if os.path.isdir(src):
  553. count += self.process_directory(src, dst)
  554. else:
  555. count += self.process_file(src, dst)
  556. return count
  557. for pfx in self.get_src_prefix(), self.get_build_prefix():
  558. try:
  559. count = try_path(os.path.join(pfx, src))
  560. except MissingError:
  561. # If src isn't a wildcard, and if that file doesn't exist in
  562. # this pfx, try next pfx.
  563. count = 0
  564. continue
  565. # Here try_path() didn't raise MissingError. Did it process any files?
  566. if count:
  567. break
  568. # Even though try_path() didn't raise MissingError, it returned 0
  569. # files. src is probably a wildcard meant for some other pfx. Loop
  570. # back to try the next.
  571. print "%d files" % count
  572. # Let caller check whether we processed as many files as expected. In
  573. # particular, let caller notice 0.
  574. return count
  575. def do(self, *actions):
  576. self.actions = actions
  577. self.construct()
  578. # perform finish actions
  579. for action in self.actions:
  580. methodname = action + "_finish"
  581. method = getattr(self, methodname, None)
  582. if method is not None:
  583. method()
  584. return self.file_list