zip.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. from __future__ import absolute_import
  2. import sys
  3. import re
  4. import fnmatch
  5. import logging
  6. import os
  7. import shutil
  8. import warnings
  9. import zipfile
  10. from pip.utils import display_path, backup_dir, rmtree
  11. from pip.utils.deprecation import RemovedInPip7Warning
  12. from pip.utils.logging import indent_log
  13. from pip.exceptions import InstallationError
  14. from pip.basecommand import Command
  15. logger = logging.getLogger(__name__)
  16. class ZipCommand(Command):
  17. """Zip individual packages."""
  18. name = 'zip'
  19. usage = """
  20. %prog [options] <package> ..."""
  21. summary = 'DEPRECATED. Zip individual packages.'
  22. def __init__(self, *args, **kw):
  23. super(ZipCommand, self).__init__(*args, **kw)
  24. if self.name == 'zip':
  25. self.cmd_opts.add_option(
  26. '--unzip',
  27. action='store_true',
  28. dest='unzip',
  29. help='Unzip (rather than zip) a package.')
  30. else:
  31. self.cmd_opts.add_option(
  32. '--zip',
  33. action='store_false',
  34. dest='unzip',
  35. default=True,
  36. help='Zip (rather than unzip) a package.')
  37. self.cmd_opts.add_option(
  38. '--no-pyc',
  39. action='store_true',
  40. dest='no_pyc',
  41. help=(
  42. 'Do not include .pyc files in zip files (useful on Google App '
  43. 'Engine).'),
  44. )
  45. self.cmd_opts.add_option(
  46. '-l', '--list',
  47. action='store_true',
  48. dest='list',
  49. help='List the packages available, and their zip status.')
  50. self.cmd_opts.add_option(
  51. '--sort-files',
  52. action='store_true',
  53. dest='sort_files',
  54. help=('With --list, sort packages according to how many files they'
  55. ' contain.'),
  56. )
  57. self.cmd_opts.add_option(
  58. '--path',
  59. action='append',
  60. dest='paths',
  61. help=('Restrict operations to the given paths (may include '
  62. 'wildcards).'),
  63. )
  64. self.cmd_opts.add_option(
  65. '-n', '--simulate',
  66. action='store_true',
  67. help='Do not actually perform the zip/unzip operation.')
  68. self.parser.insert_option_group(0, self.cmd_opts)
  69. def paths(self):
  70. """All the entries of sys.path, possibly restricted by --path"""
  71. if not self.select_paths:
  72. return sys.path
  73. result = []
  74. match_any = set()
  75. for path in sys.path:
  76. path = os.path.normcase(os.path.abspath(path))
  77. for match in self.select_paths:
  78. match = os.path.normcase(os.path.abspath(match))
  79. if '*' in match:
  80. if re.search(fnmatch.translate(match + '*'), path):
  81. result.append(path)
  82. match_any.add(match)
  83. break
  84. else:
  85. if path.startswith(match):
  86. result.append(path)
  87. match_any.add(match)
  88. break
  89. else:
  90. logger.debug(
  91. "Skipping path %s because it doesn't match %s",
  92. path,
  93. ', '.join(self.select_paths),
  94. )
  95. for match in self.select_paths:
  96. if match not in match_any and '*' not in match:
  97. result.append(match)
  98. logger.debug(
  99. "Adding path %s because it doesn't match "
  100. "anything already on sys.path",
  101. match,
  102. )
  103. return result
  104. def run(self, options, args):
  105. warnings.warn(
  106. "'pip zip' and 'pip unzip` are deprecated, and will be removed in "
  107. "a future release.",
  108. RemovedInPip7Warning,
  109. )
  110. self.select_paths = options.paths
  111. self.simulate = options.simulate
  112. if options.list:
  113. return self.list(options, args)
  114. if not args:
  115. raise InstallationError(
  116. 'You must give at least one package to zip or unzip')
  117. packages = []
  118. for arg in args:
  119. module_name, filename = self.find_package(arg)
  120. if options.unzip and os.path.isdir(filename):
  121. raise InstallationError(
  122. 'The module %s (in %s) is not a zip file; cannot be '
  123. 'unzipped' % (module_name, filename)
  124. )
  125. elif not options.unzip and not os.path.isdir(filename):
  126. raise InstallationError(
  127. 'The module %s (in %s) is not a directory; cannot be '
  128. 'zipped' % (module_name, filename)
  129. )
  130. packages.append((module_name, filename))
  131. last_status = None
  132. for module_name, filename in packages:
  133. if options.unzip:
  134. last_status = self.unzip_package(module_name, filename)
  135. else:
  136. last_status = self.zip_package(
  137. module_name, filename, options.no_pyc
  138. )
  139. return last_status
  140. def unzip_package(self, module_name, filename):
  141. zip_filename = os.path.dirname(filename)
  142. if (not os.path.isfile(zip_filename) and
  143. zipfile.is_zipfile(zip_filename)):
  144. raise InstallationError(
  145. 'Module %s (in %s) isn\'t located in a zip file in %s'
  146. % (module_name, filename, zip_filename))
  147. package_path = os.path.dirname(zip_filename)
  148. if package_path not in self.paths():
  149. logger.warning(
  150. 'Unpacking %s into %s, but %s is not on sys.path',
  151. display_path(zip_filename),
  152. display_path(package_path),
  153. display_path(package_path),
  154. )
  155. logger.info(
  156. 'Unzipping %s (in %s)', module_name, display_path(zip_filename),
  157. )
  158. if self.simulate:
  159. logger.info(
  160. 'Skipping remaining operations because of --simulate'
  161. )
  162. return
  163. with indent_log():
  164. # FIXME: this should be undoable:
  165. zip = zipfile.ZipFile(zip_filename)
  166. to_save = []
  167. for info in zip.infolist():
  168. name = info.filename
  169. if name.startswith(module_name + os.path.sep):
  170. content = zip.read(name)
  171. dest = os.path.join(package_path, name)
  172. if not os.path.exists(os.path.dirname(dest)):
  173. os.makedirs(os.path.dirname(dest))
  174. if not content and dest.endswith(os.path.sep):
  175. if not os.path.exists(dest):
  176. os.makedirs(dest)
  177. else:
  178. with open(dest, 'wb') as f:
  179. f.write(content)
  180. else:
  181. to_save.append((name, zip.read(name)))
  182. zip.close()
  183. if not to_save:
  184. logger.debug(
  185. 'Removing now-empty zip file %s',
  186. display_path(zip_filename)
  187. )
  188. os.unlink(zip_filename)
  189. self.remove_filename_from_pth(zip_filename)
  190. else:
  191. logger.debug(
  192. 'Removing entries in %s/ from zip file %s',
  193. module_name,
  194. display_path(zip_filename),
  195. )
  196. zip = zipfile.ZipFile(zip_filename, 'w')
  197. for name, content in to_save:
  198. zip.writestr(name, content)
  199. zip.close()
  200. def zip_package(self, module_name, filename, no_pyc):
  201. logger.info('Zip %s (in %s)', module_name, display_path(filename))
  202. orig_filename = filename
  203. if filename.endswith('.egg'):
  204. dest_filename = filename
  205. else:
  206. dest_filename = filename + '.zip'
  207. with indent_log():
  208. # FIXME: I think this needs to be undoable:
  209. if filename == dest_filename:
  210. filename = backup_dir(orig_filename)
  211. logger.info(
  212. 'Moving %s aside to %s', orig_filename, filename,
  213. )
  214. if not self.simulate:
  215. shutil.move(orig_filename, filename)
  216. try:
  217. logger.debug(
  218. 'Creating zip file in %s', display_path(dest_filename),
  219. )
  220. if not self.simulate:
  221. zip = zipfile.ZipFile(dest_filename, 'w')
  222. zip.writestr(module_name + '/', '')
  223. for dirpath, dirnames, filenames in os.walk(filename):
  224. if no_pyc:
  225. filenames = [f for f in filenames
  226. if not f.lower().endswith('.pyc')]
  227. for fns, is_dir in [
  228. (dirnames, True), (filenames, False)]:
  229. for fn in fns:
  230. full = os.path.join(dirpath, fn)
  231. dest = os.path.join(
  232. module_name,
  233. dirpath[len(filename):].lstrip(
  234. os.path.sep
  235. ),
  236. fn,
  237. )
  238. if is_dir:
  239. zip.writestr(dest + '/', '')
  240. else:
  241. zip.write(full, dest)
  242. zip.close()
  243. logger.debug(
  244. 'Removing old directory %s', display_path(filename),
  245. )
  246. if not self.simulate:
  247. rmtree(filename)
  248. except:
  249. # FIXME: need to do an undo here
  250. raise
  251. # FIXME: should also be undone:
  252. self.add_filename_to_pth(dest_filename)
  253. def remove_filename_from_pth(self, filename):
  254. for pth in self.pth_files():
  255. with open(pth, 'r') as f:
  256. lines = f.readlines()
  257. new_lines = [
  258. l for l in lines if l.strip() != filename]
  259. if lines != new_lines:
  260. logger.debug(
  261. 'Removing reference to %s from .pth file %s',
  262. display_path(filename),
  263. display_path(pth),
  264. )
  265. if not [line for line in new_lines if line]:
  266. logger.debug(
  267. '%s file would be empty: deleting', display_path(pth)
  268. )
  269. if not self.simulate:
  270. os.unlink(pth)
  271. else:
  272. if not self.simulate:
  273. with open(pth, 'wb') as f:
  274. f.writelines(new_lines)
  275. return
  276. logger.warning(
  277. 'Cannot find a reference to %s in any .pth file',
  278. display_path(filename),
  279. )
  280. def add_filename_to_pth(self, filename):
  281. path = os.path.dirname(filename)
  282. dest = filename + '.pth'
  283. if path not in self.paths():
  284. logger.warning(
  285. 'Adding .pth file %s, but it is not on sys.path',
  286. display_path(dest),
  287. )
  288. if not self.simulate:
  289. if os.path.exists(dest):
  290. with open(dest) as f:
  291. lines = f.readlines()
  292. if lines and not lines[-1].endswith('\n'):
  293. lines[-1] += '\n'
  294. lines.append(filename + '\n')
  295. else:
  296. lines = [filename + '\n']
  297. with open(dest, 'wb') as f:
  298. f.writelines(lines)
  299. def pth_files(self):
  300. for path in self.paths():
  301. if not os.path.exists(path) or not os.path.isdir(path):
  302. continue
  303. for filename in os.listdir(path):
  304. if filename.endswith('.pth'):
  305. yield os.path.join(path, filename)
  306. def find_package(self, package):
  307. for path in self.paths():
  308. full = os.path.join(path, package)
  309. if os.path.exists(full):
  310. return package, full
  311. if not os.path.isdir(path) and zipfile.is_zipfile(path):
  312. zip = zipfile.ZipFile(path, 'r')
  313. try:
  314. zip.read(os.path.join(package, '__init__.py'))
  315. except KeyError:
  316. pass
  317. else:
  318. zip.close()
  319. return package, full
  320. zip.close()
  321. # FIXME: need special error for package.py case:
  322. raise InstallationError(
  323. 'No package with the name %s found' % package)
  324. def list(self, options, args):
  325. if args:
  326. raise InstallationError(
  327. 'You cannot give an argument with --list')
  328. for path in sorted(self.paths()):
  329. if not os.path.exists(path):
  330. continue
  331. basename = os.path.basename(path.rstrip(os.path.sep))
  332. if os.path.isfile(path) and zipfile.is_zipfile(path):
  333. if os.path.dirname(path) not in self.paths():
  334. logger.info('Zipped egg: %s', display_path(path))
  335. continue
  336. if (basename != 'site-packages' and
  337. basename != 'dist-packages' and not
  338. path.replace('\\', '/').endswith('lib/python')):
  339. continue
  340. logger.info('In %s:', display_path(path))
  341. with indent_log():
  342. zipped = []
  343. unzipped = []
  344. for filename in sorted(os.listdir(path)):
  345. ext = os.path.splitext(filename)[1].lower()
  346. if ext in ('.pth', '.egg-info', '.egg-link'):
  347. continue
  348. if ext == '.py':
  349. logger.debug(
  350. 'Not displaying %s: not a package',
  351. display_path(filename)
  352. )
  353. continue
  354. full = os.path.join(path, filename)
  355. if os.path.isdir(full):
  356. unzipped.append((filename, self.count_package(full)))
  357. elif zipfile.is_zipfile(full):
  358. zipped.append(filename)
  359. else:
  360. logger.debug(
  361. 'Unknown file: %s', display_path(filename),
  362. )
  363. if zipped:
  364. logger.info('Zipped packages:')
  365. with indent_log():
  366. for filename in zipped:
  367. logger.info(filename)
  368. else:
  369. logger.info('No zipped packages.')
  370. if unzipped:
  371. if options.sort_files:
  372. unzipped.sort(key=lambda x: -x[1])
  373. logger.info('Unzipped packages:')
  374. with indent_log():
  375. for filename, count in unzipped:
  376. logger.info('%s (%i files)', filename, count)
  377. else:
  378. logger.info('No unzipped packages.')
  379. def count_package(self, path):
  380. total = 0
  381. for dirpath, dirnames, filenames in os.walk(path):
  382. filenames = [f for f in filenames
  383. if not f.lower().endswith('.pyc')]
  384. total += len(filenames)
  385. return total