llmanifest3.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. """\
  2. @file llmanifest3.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. (c) 2009-2024 Henri Beauchamp
  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... HB
  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(r"LL_VERSION_MAJOR\s=\s([0-9]+)", contents).group(1)
  87. minor = re.search(r"LL_VERSION_MINOR\s=\s([0-9]+)", contents).group(1)
  88. patch = re.search(r"LL_VERSION_BRANCH\s=\s([0-9]+)", contents).group(1)
  89. build = re.search(r"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, metaclass=LLManifestRegistry):
  208. manifests = {}
  209. def for_platform(self, platform):
  210. return self.manifests[platform.lower()]
  211. for_platform = classmethod(for_platform)
  212. def __init__(self, args):
  213. super(LLManifest, self).__init__()
  214. self.args = args
  215. self.file_list = []
  216. self.excludes = []
  217. self.actions = []
  218. self.src_prefix = [args['source']]
  219. self.build_prefix = [args['build']]
  220. self.dst_prefix = [args['dest']]
  221. self.created_paths = []
  222. self.package_name = "Unknown"
  223. def construct(self):
  224. """ Meant to be overriden by LLManifest implementors with code that
  225. constructs the complete destination hierarchy."""
  226. pass # override this method
  227. def exclude(self, glob):
  228. """ Excludes all files that match the glob from being included
  229. in the file list by path()."""
  230. self.excludes.append(glob)
  231. def prefix(self, src='', build=None, dst=None):
  232. """ Pushes a prefix onto the stack. Until end_prefix is
  233. called, all relevant method calls (esp. to path()) will prefix
  234. paths with the entire prefix stack. Source and destination
  235. prefixes can be different, though if only one is provided they
  236. are both equal. To specify a no-op, use an empty string, not
  237. None."""
  238. if dst is None:
  239. dst = src
  240. if build is None:
  241. build = src
  242. self.src_prefix.append(src)
  243. self.build_prefix.append(build)
  244. self.dst_prefix.append(dst)
  245. return True # so that you can wrap it in an if to get indentation
  246. def end_prefix(self, descr=None):
  247. """Pops a prefix off the stack. If given an argument, checks
  248. the argument against the top of the stack. If the argument
  249. matches neither the source or destination prefixes at the top
  250. of the stack, then misnesting must have occurred and an
  251. exception is raised."""
  252. # 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.
  253. src = self.src_prefix.pop()
  254. build = self.build_prefix.pop()
  255. dst = self.dst_prefix.pop()
  256. if descr and not(src == descr or build == descr or dst == descr):
  257. raise ValueError("End prefix '" + descr + "' didn't match '" +src+ "' or '" +dst + "'")
  258. def get_src_prefix(self):
  259. """ Returns the current source prefix."""
  260. return os.path.join(*self.src_prefix)
  261. def get_build_prefix(self):
  262. """ Returns the current build prefix."""
  263. return os.path.join(*self.build_prefix)
  264. def get_dst_prefix(self):
  265. """ Returns the current destination prefix."""
  266. return os.path.join(*self.dst_prefix)
  267. def src_path_of(self, relpath):
  268. """Returns the full path to a file or directory specified
  269. relative to the source directory."""
  270. return os.path.join(self.get_src_prefix(), relpath)
  271. def build_path_of(self, relpath):
  272. """Returns the full path to a file or directory specified
  273. relative to the build directory."""
  274. return os.path.join(self.get_build_prefix(), relpath)
  275. def dst_path_of(self, relpath):
  276. """Returns the full path to a file or directory specified
  277. relative to the destination directory."""
  278. return os.path.join(self.get_dst_prefix(), relpath)
  279. def ensure_src_dir(self, reldir):
  280. """Construct the path for a directory relative to the
  281. source path, and ensures that it exists. Returns the
  282. full path."""
  283. path = os.path.join(self.get_src_prefix(), reldir)
  284. self.cmakedirs(path)
  285. return path
  286. def ensure_dst_dir(self, reldir):
  287. """Construct the path for a directory relative to the
  288. destination path, and ensures that it exists. Returns the
  289. full path."""
  290. path = os.path.join(self.get_dst_prefix(), reldir)
  291. self.cmakedirs(path)
  292. return path
  293. def run_command(self, command):
  294. """ Runs an external command, and returns the output. Raises
  295. an exception if the command reurns a nonzero status code. For
  296. debugging/informational purpoases, prints out the command's
  297. output as it is received."""
  298. print("Running command:", command)
  299. fd = os.popen(command, 'r')
  300. lines = []
  301. while True:
  302. lines.append(fd.readline())
  303. if lines[-1] == '':
  304. break
  305. else:
  306. print(lines[-1], end=' ')
  307. output = ''.join(lines)
  308. status = fd.close()
  309. if status:
  310. raise RuntimeError(
  311. "Command %s returned non-zero status (%s) \noutput:\n%s"
  312. % (command, status, output) )
  313. return output
  314. def created_path(self, path):
  315. """ Declare that you've created a path in order to
  316. a) verify that you really have created it
  317. b) schedule it for cleanup"""
  318. if not os.path.exists(path):
  319. raise RuntimeError("Should be something at path " + path)
  320. self.created_paths.append(path)
  321. def put_in_file(self, contents, dst, src=None):
  322. # write contents as dst
  323. dst_path = self.dst_path_of(dst)
  324. f = open(dst_path, "wb")
  325. try:
  326. f.write(contents)
  327. finally:
  328. f.close()
  329. # Why would we create a file in the destination tree if not to include
  330. # it in the installer? The default src=None (plus the fact that the
  331. # src param is last) is to preserve backwards compatibility.
  332. if src:
  333. self.file_list.append([src, dst_path])
  334. return dst_path
  335. def replace_in(self, src, dst=None, searchdict={}):
  336. if dst == None:
  337. dst = src
  338. # read src
  339. f = open(self.src_path_of(src), "rbU")
  340. contents = f.read()
  341. f.close()
  342. # apply dict replacements
  343. for old, new in searchdict.items():
  344. contents = contents.replace(old, new)
  345. self.put_in_file(contents, dst)
  346. self.created_paths.append(dst)
  347. def copy_action(self, src, dst):
  348. if src and (os.path.exists(src) or os.path.islink(src)):
  349. # ensure that destination path exists
  350. self.cmakedirs(os.path.dirname(dst))
  351. self.created_paths.append(dst)
  352. if not os.path.isdir(src):
  353. self.ccopy(src,dst)
  354. else:
  355. # src is a dir
  356. self.ccopytree(src,dst)
  357. else:
  358. print("Doesn't exist:", src)
  359. def package_action(self, src, dst):
  360. pass
  361. def copy_finish(self):
  362. pass
  363. def package_finish(self):
  364. pass
  365. def unpacked_finish(self):
  366. unpacked_file_name = "unpacked_%(plat)s_%(vers)s.tar" % {
  367. 'plat':self.args['platform'],
  368. 'vers':'_'.join(self.args['version'])}
  369. print("Creating unpacked file:", unpacked_file_name)
  370. # could add a gz here but that doubles the time it takes to do this step
  371. tf = tarfile.open(self.src_path_of(unpacked_file_name), 'w:')
  372. # add the entire installation package, at the very top level
  373. tf.add(self.get_dst_prefix(), "")
  374. tf.close()
  375. def cleanup_finish(self):
  376. """ Delete paths that were specified to have been created by this script"""
  377. for c in self.created_paths:
  378. # *TODO is this gonna be useful?
  379. print("Cleaning up " + c)
  380. def process_file(self, src, dst):
  381. if self.includes(src, dst):
  382. # print src, "=>", dst
  383. for action in self.actions:
  384. methodname = action + "_action"
  385. method = getattr(self, methodname, None)
  386. if method is not None:
  387. method(src, dst)
  388. self.file_list.append([src, dst])
  389. return 1
  390. else:
  391. sys.stdout.write(" (excluding %r, %r)" % (src, dst))
  392. sys.stdout.flush()
  393. return 0
  394. def process_directory(self, src, dst):
  395. if not self.includes(src, dst):
  396. sys.stdout.write(" (excluding %r, %r)" % (src, dst))
  397. sys.stdout.flush()
  398. return 0
  399. names = os.listdir(src)
  400. self.cmakedirs(dst)
  401. errors = []
  402. count = 0
  403. for name in names:
  404. srcname = os.path.join(src, name)
  405. dstname = os.path.join(dst, name)
  406. if os.path.isdir(srcname):
  407. count += self.process_directory(srcname, dstname)
  408. else:
  409. count += self.process_file(srcname, dstname)
  410. return count
  411. def includes(self, src, dst):
  412. if src:
  413. for excl in self.excludes:
  414. if fnmatch.fnmatch(src, excl):
  415. return False
  416. return True
  417. def remove(self, *paths):
  418. for path in paths:
  419. if os.path.exists(path):
  420. print("Removing path", path)
  421. if os.path.isdir(path):
  422. shutil.rmtree(path)
  423. else:
  424. os.remove(path)
  425. def ccopy(self, src, dst):
  426. """ Copy a single file or symlink. Uses filecmp to skip copying for existing files."""
  427. if os.path.islink(src):
  428. linkto = os.readlink(src)
  429. if os.path.islink(dst) or os.path.exists(dst):
  430. os.remove(dst) # because symlinking over an existing link fails
  431. os.symlink(linkto, dst)
  432. else:
  433. # Don't recopy file if it's up-to-date.
  434. # If we seem to be not not overwriting files that have been
  435. # updated, set the last arg to False, but it will take longer.
  436. if os.path.exists(dst) and filecmp.cmp(src, dst, True):
  437. return
  438. # only copy if it's not excluded
  439. if self.includes(src, dst):
  440. try:
  441. os.unlink(dst)
  442. except OSError as err:
  443. if err.errno != errno.ENOENT:
  444. raise
  445. shutil.copy2(src, dst)
  446. def ccopytree(self, src, dst):
  447. """Direct copy of shutil.copytree with the additional
  448. feature that the destination directory can exist. It
  449. is so dumb that Python doesn't come with this. Also it
  450. implements the excludes functionality."""
  451. if not self.includes(src, dst):
  452. return
  453. names = os.listdir(src)
  454. self.cmakedirs(dst)
  455. errors = []
  456. for name in names:
  457. srcname = os.path.join(src, name)
  458. dstname = os.path.join(dst, name)
  459. try:
  460. if os.path.isdir(srcname):
  461. self.ccopytree(srcname, dstname)
  462. else:
  463. self.ccopy(srcname, dstname)
  464. # XXX What about devices, sockets etc.?
  465. except (IOError, os.error) as why:
  466. errors.append((srcname, dstname, why))
  467. if errors:
  468. raise RuntimeError(errors)
  469. def cmakedirs(self, path):
  470. """Ensures that a directory exists, and doesn't throw an exception
  471. if you call it on an existing directory."""
  472. # print "making path: ", path
  473. path = os.path.normpath(path)
  474. self.created_paths.append(path)
  475. if not os.path.exists(path):
  476. os.makedirs(path)
  477. def find_existing_file(self, *list):
  478. for f in list:
  479. if os.path.exists(f):
  480. return f
  481. # didn't find it, return last item in list
  482. if len(list) > 0:
  483. return list[-1]
  484. else:
  485. return None
  486. def contents_of_tar(self, src_tar, dst_dir):
  487. """ Extracts the contents of the tarfile (specified
  488. relative to the source prefix) into the directory
  489. specified relative to the destination directory."""
  490. self.check_file_exists(src_tar)
  491. tf = tarfile.open(self.src_path_of(src_tar), 'r')
  492. for member in tf.getmembers():
  493. tf.extract(member, self.ensure_dst_dir(dst_dir))
  494. # TODO get actions working on these dudes, perhaps we should extract to a temporary directory and then process_directory on it?
  495. self.file_list.append([src_tar,
  496. self.dst_path_of(os.path.join(dst_dir,member.name))])
  497. tf.close()
  498. def wildcard_regex(self, src_glob, dst_glob):
  499. src_re = re.escape(src_glob)
  500. src_re = src_re.replace('\\*', '([-a-zA-Z0-9._ ]*)')
  501. dst_temp = dst_glob
  502. i = 1
  503. while dst_temp.count("*") > 0:
  504. dst_temp = dst_temp.replace('*', '\\g<' + str(i) + '>', 1)
  505. i = i+1
  506. return re.compile(src_re), dst_temp
  507. def check_file_exists(self, path):
  508. if not os.path.exists(path) and not os.path.islink(path):
  509. raise MissingError("Path %s doesn't exist" % (os.path.abspath(path),))
  510. wildcard_pattern = re.compile('\\*')
  511. def expand_globs(self, src, dst):
  512. src_list = glob.glob(src)
  513. src_re, d_template = self.wildcard_regex(src.replace('\\', '/'),
  514. dst.replace('\\', '/'))
  515. for s in src_list:
  516. d = src_re.sub(d_template, s.replace('\\', '/'))
  517. yield os.path.normpath(s), os.path.normpath(d)
  518. def path2basename(self, path, file):
  519. """
  520. It is a common idiom to write:
  521. self.path(os.path.join(somedir, somefile), somefile)
  522. So instead you can write:
  523. self.path2basename(somedir, somefile)
  524. Note that this is NOT the same as:
  525. self.path(os.path.join(somedir, somefile))
  526. which is the same as:
  527. temppath = os.path.join(somedir, somefile)
  528. self.path(temppath, temppath)
  529. """
  530. return self.path(os.path.join(path, file), file)
  531. def path(self, src, dst=None):
  532. sys.stdout.write("Processing %s => %s ... " % (src, dst))
  533. sys.stdout.flush()
  534. if src == None:
  535. raise ManifestError("No source file, dst is " + dst)
  536. if dst == None:
  537. dst = src
  538. dst = os.path.join(self.get_dst_prefix(), dst)
  539. def try_path(src):
  540. # expand globs
  541. count = 0
  542. if self.wildcard_pattern.search(src):
  543. for s,d in self.expand_globs(src, dst):
  544. assert(s != d)
  545. count += self.process_file(s, d)
  546. else:
  547. # if we're specifying a single path (not a glob),
  548. # we should error out if it doesn't exist
  549. self.check_file_exists(src)
  550. # if it's a directory, recurse through it
  551. if os.path.isdir(src):
  552. count += self.process_directory(src, dst)
  553. else:
  554. count += self.process_file(src, dst)
  555. return count
  556. for pfx in self.get_src_prefix(), self.get_build_prefix():
  557. try:
  558. count = try_path(os.path.join(pfx, src))
  559. except MissingError:
  560. # If src isn't a wildcard, and if that file doesn't exist in
  561. # this pfx, try next pfx.
  562. count = 0
  563. continue
  564. # Here try_path() didn't raise MissingError. Did it process any files?
  565. if count:
  566. break
  567. # Even though try_path() didn't raise MissingError, it returned 0
  568. # files. src is probably a wildcard meant for some other pfx. Loop
  569. # back to try the next.
  570. print("%d files" % count)
  571. # Let caller check whether we processed as many files as expected. In
  572. # particular, let caller notice 0.
  573. return count
  574. def do(self, *actions):
  575. self.actions = actions
  576. self.construct()
  577. # perform finish actions
  578. for action in self.actions:
  579. methodname = action + "_finish"
  580. method = getattr(self, methodname, None)
  581. if method is not None:
  582. method()
  583. return self.file_list