|
1 #!/usr/bin/env python |
|
2 import sys |
|
3 import os |
|
4 import errno |
|
5 import stat |
|
6 import optparse |
|
7 import pkg_resources |
|
8 import urllib2 |
|
9 import urllib |
|
10 import mimetypes |
|
11 import zipfile |
|
12 import tarfile |
|
13 import tempfile |
|
14 import subprocess |
|
15 import posixpath |
|
16 import re |
|
17 import shutil |
|
18 import fnmatch |
|
19 import operator |
|
20 import copy |
|
21 try: |
|
22 from hashlib import md5 |
|
23 except ImportError: |
|
24 import md5 as md5_module |
|
25 md5 = md5_module.new |
|
26 import urlparse |
|
27 from email.FeedParser import FeedParser |
|
28 import traceback |
|
29 from cStringIO import StringIO |
|
30 import socket |
|
31 from Queue import Queue |
|
32 from Queue import Empty as QueueEmpty |
|
33 import threading |
|
34 import httplib |
|
35 import time |
|
36 import logging |
|
37 import ConfigParser |
|
38 from distutils.util import strtobool |
|
39 from distutils import sysconfig |
|
40 |
|
41 class InstallationError(Exception): |
|
42 """General exception during installation""" |
|
43 |
|
44 class UninstallationError(Exception): |
|
45 """General exception during uninstallation""" |
|
46 |
|
47 class DistributionNotFound(InstallationError): |
|
48 """Raised when a distribution cannot be found to satisfy a requirement""" |
|
49 |
|
50 class BadCommand(Exception): |
|
51 """Raised when virtualenv or a command is not found""" |
|
52 |
|
53 try: |
|
54 any |
|
55 except NameError: |
|
56 def any(seq): |
|
57 for item in seq: |
|
58 if item: |
|
59 return True |
|
60 return False |
|
61 |
|
62 if getattr(sys, 'real_prefix', None): |
|
63 ## FIXME: is build/ a good name? |
|
64 build_prefix = os.path.join(sys.prefix, 'build') |
|
65 src_prefix = os.path.join(sys.prefix, 'src') |
|
66 else: |
|
67 ## FIXME: this isn't a very good default |
|
68 build_prefix = os.path.join(os.getcwd(), 'build') |
|
69 src_prefix = os.path.join(os.getcwd(), 'src') |
|
70 |
|
71 # FIXME doesn't account for venv linked to global site-packages |
|
72 |
|
73 site_packages = sysconfig.get_python_lib() |
|
74 user_dir = os.path.expanduser('~') |
|
75 if sys.platform == 'win32': |
|
76 bin_py = os.path.join(sys.prefix, 'Scripts') |
|
77 # buildout uses 'bin' on Windows too? |
|
78 if not os.path.exists(bin_py): |
|
79 bin_py = os.path.join(sys.prefix, 'bin') |
|
80 config_dir = os.environ.get('APPDATA', user_dir) # Use %APPDATA% for roaming |
|
81 default_config_file = os.path.join(config_dir, 'pip', 'pip.ini') |
|
82 else: |
|
83 bin_py = os.path.join(sys.prefix, 'bin') |
|
84 default_config_file = os.path.join(user_dir, '.pip', 'pip.conf') |
|
85 # Forcing to use /usr/local/bin for standard Mac OS X framework installs |
|
86 if sys.platform[:6] == 'darwin' and sys.prefix[:16] == '/System/Library/': |
|
87 bin_py = '/usr/local/bin' |
|
88 |
|
89 class UpdatingDefaultsHelpFormatter(optparse.IndentedHelpFormatter): |
|
90 """Custom help formatter for use in ConfigOptionParser that updates |
|
91 the defaults before expanding them, allowing them to show up correctly |
|
92 in the help listing""" |
|
93 |
|
94 def expand_default(self, option): |
|
95 if self.parser is not None: |
|
96 self.parser.update_defaults(self.parser.defaults) |
|
97 return optparse.IndentedHelpFormatter.expand_default(self, option) |
|
98 |
|
99 |
|
100 class ConfigOptionParser(optparse.OptionParser): |
|
101 """Custom option parser which updates its defaults by by checking the |
|
102 configuration files and environmental variables""" |
|
103 |
|
104 def __init__(self, *args, **kwargs): |
|
105 self.config = ConfigParser.RawConfigParser() |
|
106 self.name = kwargs.pop('name') |
|
107 self.files = self.get_config_files() |
|
108 self.config.read(self.files) |
|
109 assert self.name |
|
110 optparse.OptionParser.__init__(self, *args, **kwargs) |
|
111 |
|
112 def get_config_files(self): |
|
113 config_file = os.environ.get('PIP_CONFIG_FILE', False) |
|
114 if config_file and os.path.exists(config_file): |
|
115 return [config_file] |
|
116 return [default_config_file] |
|
117 |
|
118 def update_defaults(self, defaults): |
|
119 """Updates the given defaults with values from the config files and |
|
120 the environ. Does a little special handling for certain types of |
|
121 options (lists).""" |
|
122 # Then go and look for the other sources of configuration: |
|
123 config = {} |
|
124 # 1. config files |
|
125 for section in ('global', self.name): |
|
126 config.update(dict(self.get_config_section(section))) |
|
127 # 2. environmental variables |
|
128 config.update(dict(self.get_environ_vars())) |
|
129 # Then set the options with those values |
|
130 for key, val in config.iteritems(): |
|
131 key = key.replace('_', '-') |
|
132 if not key.startswith('--'): |
|
133 key = '--%s' % key # only prefer long opts |
|
134 option = self.get_option(key) |
|
135 if option is not None: |
|
136 # ignore empty values |
|
137 if not val: |
|
138 continue |
|
139 # handle multiline configs |
|
140 if option.action == 'append': |
|
141 val = val.split() |
|
142 else: |
|
143 option.nargs = 1 |
|
144 if option.action in ('store_true', 'store_false', 'count'): |
|
145 val = strtobool(val) |
|
146 try: |
|
147 val = option.convert_value(key, val) |
|
148 except optparse.OptionValueError, e: |
|
149 print ("An error occured during configuration: %s" % e) |
|
150 sys.exit(3) |
|
151 defaults[option.dest] = val |
|
152 return defaults |
|
153 |
|
154 def get_config_section(self, name): |
|
155 """Get a section of a configuration""" |
|
156 if self.config.has_section(name): |
|
157 return self.config.items(name) |
|
158 return [] |
|
159 |
|
160 def get_environ_vars(self, prefix='PIP_'): |
|
161 """Returns a generator with all environmental vars with prefix PIP_""" |
|
162 for key, val in os.environ.iteritems(): |
|
163 if key.startswith(prefix): |
|
164 yield (key.replace(prefix, '').lower(), val) |
|
165 |
|
166 def get_default_values(self): |
|
167 """Overridding to make updating the defaults after instantiation of |
|
168 the option parser possible, update_defaults() does the dirty work.""" |
|
169 if not self.process_default_values: |
|
170 # Old, pre-Optik 1.5 behaviour. |
|
171 return optparse.Values(self.defaults) |
|
172 |
|
173 defaults = self.update_defaults(self.defaults.copy()) # ours |
|
174 for option in self._get_all_options(): |
|
175 default = defaults.get(option.dest) |
|
176 if isinstance(default, basestring): |
|
177 opt_str = option.get_opt_string() |
|
178 defaults[option.dest] = option.check_value(opt_str, default) |
|
179 return optparse.Values(defaults) |
|
180 |
|
181 try: |
|
182 pip_dist = pkg_resources.get_distribution('pip') |
|
183 version = '%s from %s (python %s)' % ( |
|
184 pip_dist, pip_dist.location, sys.version[:3]) |
|
185 except pkg_resources.DistributionNotFound: |
|
186 # when running pip.py without installing |
|
187 version=None |
|
188 |
|
189 def rmtree_errorhandler(func, path, exc_info): |
|
190 """On Windows, the files in .svn are read-only, so when rmtree() tries to |
|
191 remove them, an exception is thrown. We catch that here, remove the |
|
192 read-only attribute, and hopefully continue without problems.""" |
|
193 exctype, value = exc_info[:2] |
|
194 # lookin for a windows error |
|
195 if exctype is not WindowsError or 'Access is denied' not in str(value): |
|
196 raise |
|
197 # file type should currently be read only |
|
198 if ((os.stat(path).st_mode & stat.S_IREAD) != stat.S_IREAD): |
|
199 raise |
|
200 # convert to read/write |
|
201 os.chmod(path, stat.S_IWRITE) |
|
202 # use the original function to repeat the operation |
|
203 func(path) |
|
204 |
|
205 class VcsSupport(object): |
|
206 _registry = {} |
|
207 schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp'] |
|
208 |
|
209 def __init__(self): |
|
210 # Register more schemes with urlparse for various version control systems |
|
211 urlparse.uses_netloc.extend(self.schemes) |
|
212 urlparse.uses_fragment.extend(self.schemes) |
|
213 super(VcsSupport, self).__init__() |
|
214 |
|
215 def __iter__(self): |
|
216 return self._registry.__iter__() |
|
217 |
|
218 @property |
|
219 def backends(self): |
|
220 return self._registry.values() |
|
221 |
|
222 @property |
|
223 def dirnames(self): |
|
224 return [backend.dirname for backend in self.backends] |
|
225 |
|
226 @property |
|
227 def all_schemes(self): |
|
228 schemes = [] |
|
229 for backend in self.backends: |
|
230 schemes.extend(backend.schemes) |
|
231 return schemes |
|
232 |
|
233 def register(self, cls): |
|
234 if not hasattr(cls, 'name'): |
|
235 logger.warn('Cannot register VCS %s' % cls.__name__) |
|
236 return |
|
237 if cls.name not in self._registry: |
|
238 self._registry[cls.name] = cls |
|
239 |
|
240 def unregister(self, cls=None, name=None): |
|
241 if name in self._registry: |
|
242 del self._registry[name] |
|
243 elif cls in self._registry.values(): |
|
244 del self._registry[cls.name] |
|
245 else: |
|
246 logger.warn('Cannot unregister because no class or name given') |
|
247 |
|
248 def get_backend_name(self, location): |
|
249 """ |
|
250 Return the name of the version control backend if found at given |
|
251 location, e.g. vcs.get_backend_name('/path/to/vcs/checkout') |
|
252 """ |
|
253 for vc_type in self._registry.values(): |
|
254 path = os.path.join(location, vc_type.dirname) |
|
255 if os.path.exists(path): |
|
256 return vc_type.name |
|
257 return None |
|
258 |
|
259 def get_backend(self, name): |
|
260 name = name.lower() |
|
261 if name in self._registry: |
|
262 return self._registry[name] |
|
263 |
|
264 def get_backend_from_location(self, location): |
|
265 vc_type = self.get_backend_name(location) |
|
266 if vc_type: |
|
267 return self.get_backend(vc_type) |
|
268 return None |
|
269 |
|
270 vcs = VcsSupport() |
|
271 |
|
272 parser = ConfigOptionParser( |
|
273 usage='%prog COMMAND [OPTIONS]', |
|
274 version=version, |
|
275 add_help_option=False, |
|
276 formatter=UpdatingDefaultsHelpFormatter(), |
|
277 name='global') |
|
278 |
|
279 parser.add_option( |
|
280 '-h', '--help', |
|
281 dest='help', |
|
282 action='store_true', |
|
283 help='Show help') |
|
284 parser.add_option( |
|
285 '-E', '--environment', |
|
286 dest='venv', |
|
287 metavar='DIR', |
|
288 help='virtualenv environment to run pip in (either give the ' |
|
289 'interpreter or the environment base directory)') |
|
290 parser.add_option( |
|
291 '-s', '--enable-site-packages', |
|
292 dest='site_packages', |
|
293 action='store_true', |
|
294 help='Include site-packages in virtualenv if one is to be ' |
|
295 'created. Ignored if --environment is not used or ' |
|
296 'the virtualenv already exists.') |
|
297 parser.add_option( |
|
298 # Defines a default root directory for virtualenvs, relative |
|
299 # virtualenvs names/paths are considered relative to it. |
|
300 '--virtualenv-base', |
|
301 dest='venv_base', |
|
302 type='str', |
|
303 default='', |
|
304 help=optparse.SUPPRESS_HELP) |
|
305 parser.add_option( |
|
306 # Run only if inside a virtualenv, bail if not. |
|
307 '--require-virtualenv', '--require-venv', |
|
308 dest='require_venv', |
|
309 action='store_true', |
|
310 default=False, |
|
311 help=optparse.SUPPRESS_HELP) |
|
312 parser.add_option( |
|
313 # Use automatically an activated virtualenv instead of installing |
|
314 # globally. -E will be ignored if used. |
|
315 '--respect-virtualenv', '--respect-venv', |
|
316 dest='respect_venv', |
|
317 action='store_true', |
|
318 default=False, |
|
319 help=optparse.SUPPRESS_HELP) |
|
320 |
|
321 parser.add_option( |
|
322 '-v', '--verbose', |
|
323 dest='verbose', |
|
324 action='count', |
|
325 default=0, |
|
326 help='Give more output') |
|
327 parser.add_option( |
|
328 '-q', '--quiet', |
|
329 dest='quiet', |
|
330 action='count', |
|
331 default=0, |
|
332 help='Give less output') |
|
333 parser.add_option( |
|
334 '--log', |
|
335 dest='log', |
|
336 metavar='FILENAME', |
|
337 help='Log file where a complete (maximum verbosity) record will be kept') |
|
338 parser.add_option( |
|
339 # Writes the log levels explicitely to the log' |
|
340 '--log-explicit-levels', |
|
341 dest='log_explicit_levels', |
|
342 action='store_true', |
|
343 default=False, |
|
344 help=optparse.SUPPRESS_HELP) |
|
345 parser.add_option( |
|
346 # The default log file |
|
347 '--local-log', '--log-file', |
|
348 dest='log_file', |
|
349 metavar='FILENAME', |
|
350 default='./pip-log.txt', |
|
351 help=optparse.SUPPRESS_HELP) |
|
352 |
|
353 parser.add_option( |
|
354 '--proxy', |
|
355 dest='proxy', |
|
356 type='str', |
|
357 default='', |
|
358 help="Specify a proxy in the form user:passwd@proxy.server:port. " |
|
359 "Note that the user:password@ is optional and required only if you " |
|
360 "are behind an authenticated proxy. If you provide " |
|
361 "user@proxy.server:port then you will be prompted for a password.") |
|
362 parser.add_option( |
|
363 '--timeout', '--default-timeout', |
|
364 metavar='SECONDS', |
|
365 dest='timeout', |
|
366 type='float', |
|
367 default=15, |
|
368 help='Set the socket timeout (default %default seconds)') |
|
369 parser.add_option( |
|
370 # The default version control system for editables, e.g. 'svn' |
|
371 '--default-vcs', |
|
372 dest='default_vcs', |
|
373 type='str', |
|
374 default='', |
|
375 help=optparse.SUPPRESS_HELP) |
|
376 parser.add_option( |
|
377 # A regex to be used to skip requirements |
|
378 '--skip-requirements-regex', |
|
379 dest='skip_requirements_regex', |
|
380 type='str', |
|
381 default='', |
|
382 help=optparse.SUPPRESS_HELP) |
|
383 |
|
384 parser.disable_interspersed_args() |
|
385 |
|
386 _commands = {} |
|
387 |
|
388 class Command(object): |
|
389 name = None |
|
390 usage = None |
|
391 hidden = False |
|
392 def __init__(self): |
|
393 assert self.name |
|
394 self.parser = ConfigOptionParser( |
|
395 usage=self.usage, |
|
396 prog='%s %s' % (sys.argv[0], self.name), |
|
397 version=parser.version, |
|
398 formatter=UpdatingDefaultsHelpFormatter(), |
|
399 name=self.name) |
|
400 for option in parser.option_list: |
|
401 if not option.dest or option.dest == 'help': |
|
402 # -h, --version, etc |
|
403 continue |
|
404 self.parser.add_option(option) |
|
405 _commands[self.name] = self |
|
406 |
|
407 def merge_options(self, initial_options, options): |
|
408 # Make sure we have all global options carried over |
|
409 for attr in ['log', 'venv', 'proxy', 'venv_base', 'require_venv', |
|
410 'respect_venv', 'log_explicit_levels', 'log_file', |
|
411 'timeout', 'default_vcs', 'skip_requirements_regex']: |
|
412 setattr(options, attr, getattr(initial_options, attr) or getattr(options, attr)) |
|
413 options.quiet += initial_options.quiet |
|
414 options.verbose += initial_options.verbose |
|
415 |
|
416 def main(self, complete_args, args, initial_options): |
|
417 global logger |
|
418 options, args = self.parser.parse_args(args) |
|
419 self.merge_options(initial_options, options) |
|
420 |
|
421 if options.require_venv and not options.venv: |
|
422 # If a venv is required check if it can really be found |
|
423 if not os.environ.get('VIRTUAL_ENV'): |
|
424 print 'Could not find an activated virtualenv (required).' |
|
425 sys.exit(3) |
|
426 # Automatically install in currently activated venv if required |
|
427 options.respect_venv = True |
|
428 |
|
429 if args and args[-1] == '___VENV_RESTART___': |
|
430 ## FIXME: We don't do anything this this value yet: |
|
431 venv_location = args[-2] |
|
432 args = args[:-2] |
|
433 options.venv = None |
|
434 else: |
|
435 # If given the option to respect the activated environment |
|
436 # check if no venv is given as a command line parameter |
|
437 if options.respect_venv and os.environ.get('VIRTUAL_ENV'): |
|
438 if options.venv and os.path.exists(options.venv): |
|
439 # Make sure command line venv and environmental are the same |
|
440 if (os.path.realpath(os.path.expanduser(options.venv)) != |
|
441 os.path.realpath(os.environ.get('VIRTUAL_ENV'))): |
|
442 print ("Given virtualenv (%s) doesn't match " |
|
443 "currently activated virtualenv (%s)." |
|
444 % (options.venv, os.environ.get('VIRTUAL_ENV'))) |
|
445 sys.exit(3) |
|
446 else: |
|
447 options.venv = os.environ.get('VIRTUAL_ENV') |
|
448 print 'Using already activated environment %s' % options.venv |
|
449 level = 1 # Notify |
|
450 level += options.verbose |
|
451 level -= options.quiet |
|
452 level = Logger.level_for_integer(4-level) |
|
453 complete_log = [] |
|
454 logger = Logger([(level, sys.stdout), |
|
455 (Logger.DEBUG, complete_log.append)]) |
|
456 if options.log_explicit_levels: |
|
457 logger.explicit_levels = True |
|
458 if options.venv: |
|
459 if options.verbose > 0: |
|
460 # The logger isn't setup yet |
|
461 print 'Running in environment %s' % options.venv |
|
462 site_packages=False |
|
463 if options.site_packages: |
|
464 site_packages=True |
|
465 restart_in_venv(options.venv, options.venv_base, site_packages, |
|
466 complete_args) |
|
467 # restart_in_venv should actually never return, but for clarity... |
|
468 return |
|
469 ## FIXME: not sure if this sure come before or after venv restart |
|
470 if options.log: |
|
471 log_fp = open_logfile_append(options.log) |
|
472 logger.consumers.append((logger.DEBUG, log_fp)) |
|
473 else: |
|
474 log_fp = None |
|
475 |
|
476 socket.setdefaulttimeout(options.timeout or None) |
|
477 |
|
478 setup_proxy_handler(options.proxy) |
|
479 |
|
480 exit = 0 |
|
481 try: |
|
482 self.run(options, args) |
|
483 except (InstallationError, UninstallationError), e: |
|
484 logger.fatal(str(e)) |
|
485 logger.info('Exception information:\n%s' % format_exc()) |
|
486 exit = 1 |
|
487 except: |
|
488 logger.fatal('Exception:\n%s' % format_exc()) |
|
489 exit = 2 |
|
490 |
|
491 if log_fp is not None: |
|
492 log_fp.close() |
|
493 if exit: |
|
494 log_fn = options.log_file |
|
495 text = '\n'.join(complete_log) |
|
496 logger.fatal('Storing complete log in %s' % log_fn) |
|
497 log_fp = open_logfile_append(log_fn) |
|
498 log_fp.write(text) |
|
499 log_fp.close() |
|
500 return exit |
|
501 |
|
502 class HelpCommand(Command): |
|
503 name = 'help' |
|
504 usage = '%prog' |
|
505 summary = 'Show available commands' |
|
506 |
|
507 def run(self, options, args): |
|
508 if args: |
|
509 ## FIXME: handle errors better here |
|
510 command = args[0] |
|
511 if command not in _commands: |
|
512 raise InstallationError('No command with the name: %s' % command) |
|
513 command = _commands[command] |
|
514 command.parser.print_help() |
|
515 return |
|
516 parser.print_help() |
|
517 print |
|
518 print 'Commands available:' |
|
519 commands = list(set(_commands.values())) |
|
520 commands.sort(key=lambda x: x.name) |
|
521 for command in commands: |
|
522 if command.hidden: |
|
523 continue |
|
524 print ' %s: %s' % (command.name, command.summary) |
|
525 |
|
526 HelpCommand() |
|
527 |
|
528 |
|
529 class InstallCommand(Command): |
|
530 name = 'install' |
|
531 usage = '%prog [OPTIONS] PACKAGE_NAMES...' |
|
532 summary = 'Install packages' |
|
533 bundle = False |
|
534 |
|
535 def __init__(self): |
|
536 super(InstallCommand, self).__init__() |
|
537 self.parser.add_option( |
|
538 '-e', '--editable', |
|
539 dest='editables', |
|
540 action='append', |
|
541 default=[], |
|
542 metavar='VCS+REPOS_URL[@REV]#egg=PACKAGE', |
|
543 help='Install a package directly from a checkout. Source will be checked ' |
|
544 'out into src/PACKAGE (lower-case) and installed in-place (using ' |
|
545 'setup.py develop). You can run this on an existing directory/checkout (like ' |
|
546 'pip install -e src/mycheckout). This option may be provided multiple times. ' |
|
547 'Possible values for VCS are: svn, git, hg and bzr.') |
|
548 self.parser.add_option( |
|
549 '-r', '--requirement', |
|
550 dest='requirements', |
|
551 action='append', |
|
552 default=[], |
|
553 metavar='FILENAME', |
|
554 help='Install all the packages listed in the given requirements file. ' |
|
555 'This option can be used multiple times.') |
|
556 self.parser.add_option( |
|
557 '-f', '--find-links', |
|
558 dest='find_links', |
|
559 action='append', |
|
560 default=[], |
|
561 metavar='URL', |
|
562 help='URL to look for packages at') |
|
563 self.parser.add_option( |
|
564 '-i', '--index-url', '--pypi-url', |
|
565 dest='index_url', |
|
566 metavar='URL', |
|
567 default='http://pypi.python.org/simple', |
|
568 help='Base URL of Python Package Index (default %default)') |
|
569 self.parser.add_option( |
|
570 '--extra-index-url', |
|
571 dest='extra_index_urls', |
|
572 metavar='URL', |
|
573 action='append', |
|
574 default=[], |
|
575 help='Extra URLs of package indexes to use in addition to --index-url') |
|
576 self.parser.add_option( |
|
577 '--no-index', |
|
578 dest='no_index', |
|
579 action='store_true', |
|
580 default=False, |
|
581 help='Ignore package index (only looking at --find-links URLs instead)') |
|
582 |
|
583 self.parser.add_option( |
|
584 '-b', '--build', '--build-dir', '--build-directory', |
|
585 dest='build_dir', |
|
586 metavar='DIR', |
|
587 default=None, |
|
588 help='Unpack packages into DIR (default %s) and build from there' % build_prefix) |
|
589 self.parser.add_option( |
|
590 '-d', '--download', '--download-dir', '--download-directory', |
|
591 dest='download_dir', |
|
592 metavar='DIR', |
|
593 default=None, |
|
594 help='Download packages into DIR instead of installing them') |
|
595 self.parser.add_option( |
|
596 '--download-cache', |
|
597 dest='download_cache', |
|
598 metavar='DIR', |
|
599 default=None, |
|
600 help='Cache downloaded packages in DIR') |
|
601 self.parser.add_option( |
|
602 '--src', '--source', '--source-dir', '--source-directory', |
|
603 dest='src_dir', |
|
604 metavar='DIR', |
|
605 default=None, |
|
606 help='Check out --editable packages into DIR (default %s)' % src_prefix) |
|
607 |
|
608 self.parser.add_option( |
|
609 '-U', '--upgrade', |
|
610 dest='upgrade', |
|
611 action='store_true', |
|
612 help='Upgrade all packages to the newest available version') |
|
613 self.parser.add_option( |
|
614 '-I', '--ignore-installed', |
|
615 dest='ignore_installed', |
|
616 action='store_true', |
|
617 help='Ignore the installed packages (reinstalling instead)') |
|
618 self.parser.add_option( |
|
619 '--no-deps', '--no-dependencies', |
|
620 dest='ignore_dependencies', |
|
621 action='store_true', |
|
622 default=False, |
|
623 help='Ignore package dependencies') |
|
624 self.parser.add_option( |
|
625 '--no-install', |
|
626 dest='no_install', |
|
627 action='store_true', |
|
628 help="Download and unpack all packages, but don't actually install them") |
|
629 |
|
630 self.parser.add_option( |
|
631 '--install-option', |
|
632 dest='install_options', |
|
633 action='append', |
|
634 help="Extra arguments to be supplied to the setup.py install " |
|
635 "command (use like --install-option=\"--install-scripts=/usr/local/bin\"). " |
|
636 "Use multiple --install-option options to pass multiple options to setup.py install. " |
|
637 "If you are using an option with a directory path, be sure to use absolute path.") |
|
638 |
|
639 def run(self, options, args): |
|
640 if not options.build_dir: |
|
641 options.build_dir = build_prefix |
|
642 if not options.src_dir: |
|
643 options.src_dir = src_prefix |
|
644 if options.download_dir: |
|
645 options.no_install = True |
|
646 options.ignore_installed = True |
|
647 else: |
|
648 options.build_dir = os.path.abspath(options.build_dir) |
|
649 options.src_dir = os.path.abspath(options.src_dir) |
|
650 install_options = options.install_options or [] |
|
651 index_urls = [options.index_url] + options.extra_index_urls |
|
652 if options.no_index: |
|
653 logger.notify('Ignoring indexes: %s' % ','.join(index_urls)) |
|
654 index_urls = [] |
|
655 finder = PackageFinder( |
|
656 find_links=options.find_links, |
|
657 index_urls=index_urls) |
|
658 requirement_set = RequirementSet( |
|
659 build_dir=options.build_dir, |
|
660 src_dir=options.src_dir, |
|
661 download_dir=options.download_dir, |
|
662 download_cache=options.download_cache, |
|
663 upgrade=options.upgrade, |
|
664 ignore_installed=options.ignore_installed, |
|
665 ignore_dependencies=options.ignore_dependencies) |
|
666 for name in args: |
|
667 requirement_set.add_requirement( |
|
668 InstallRequirement.from_line(name, None)) |
|
669 for name in options.editables: |
|
670 requirement_set.add_requirement( |
|
671 InstallRequirement.from_editable(name, default_vcs=options.default_vcs)) |
|
672 for filename in options.requirements: |
|
673 for req in parse_requirements(filename, finder=finder, options=options): |
|
674 requirement_set.add_requirement(req) |
|
675 requirement_set.install_files(finder, force_root_egg_info=self.bundle) |
|
676 if not options.no_install and not self.bundle: |
|
677 requirement_set.install(install_options) |
|
678 installed = ' '.join([req.name for req in |
|
679 requirement_set.successfully_installed]) |
|
680 if installed: |
|
681 logger.notify('Successfully installed %s' % installed) |
|
682 elif not self.bundle: |
|
683 downloaded = ' '.join([req.name for req in |
|
684 requirement_set.successfully_downloaded]) |
|
685 if downloaded: |
|
686 logger.notify('Successfully downloaded %s' % downloaded) |
|
687 return requirement_set |
|
688 |
|
689 InstallCommand() |
|
690 |
|
691 class UninstallCommand(Command): |
|
692 name = 'uninstall' |
|
693 usage = '%prog [OPTIONS] PACKAGE_NAMES ...' |
|
694 summary = 'Uninstall packages' |
|
695 |
|
696 def __init__(self): |
|
697 super(UninstallCommand, self).__init__() |
|
698 self.parser.add_option( |
|
699 '-r', '--requirement', |
|
700 dest='requirements', |
|
701 action='append', |
|
702 default=[], |
|
703 metavar='FILENAME', |
|
704 help='Uninstall all the packages listed in the given requirements file. ' |
|
705 'This option can be used multiple times.') |
|
706 self.parser.add_option( |
|
707 '-y', '--yes', |
|
708 dest='yes', |
|
709 action='store_true', |
|
710 help="Don't ask for confirmation of uninstall deletions.") |
|
711 |
|
712 def run(self, options, args): |
|
713 requirement_set = RequirementSet( |
|
714 build_dir=None, |
|
715 src_dir=None, |
|
716 download_dir=None) |
|
717 for name in args: |
|
718 requirement_set.add_requirement( |
|
719 InstallRequirement.from_line(name)) |
|
720 for filename in options.requirements: |
|
721 for req in parse_requirements(filename, options=options): |
|
722 requirement_set.add_requirement(req) |
|
723 requirement_set.uninstall(auto_confirm=options.yes) |
|
724 |
|
725 UninstallCommand() |
|
726 |
|
727 class BundleCommand(InstallCommand): |
|
728 name = 'bundle' |
|
729 usage = '%prog [OPTIONS] BUNDLE_NAME.pybundle PACKAGE_NAMES...' |
|
730 summary = 'Create pybundles (archives containing multiple packages)' |
|
731 bundle = True |
|
732 |
|
733 def __init__(self): |
|
734 super(BundleCommand, self).__init__() |
|
735 |
|
736 def run(self, options, args): |
|
737 if not args: |
|
738 raise InstallationError('You must give a bundle filename') |
|
739 if not options.build_dir: |
|
740 options.build_dir = backup_dir(build_prefix, '-bundle') |
|
741 if not options.src_dir: |
|
742 options.src_dir = backup_dir(src_prefix, '-bundle') |
|
743 # We have to get everything when creating a bundle: |
|
744 options.ignore_installed = True |
|
745 logger.notify('Putting temporary build files in %s and source/develop files in %s' |
|
746 % (display_path(options.build_dir), display_path(options.src_dir))) |
|
747 bundle_filename = args[0] |
|
748 args = args[1:] |
|
749 requirement_set = super(BundleCommand, self).run(options, args) |
|
750 # FIXME: here it has to do something |
|
751 requirement_set.create_bundle(bundle_filename) |
|
752 logger.notify('Created bundle in %s' % bundle_filename) |
|
753 return requirement_set |
|
754 |
|
755 BundleCommand() |
|
756 |
|
757 |
|
758 class FreezeCommand(Command): |
|
759 name = 'freeze' |
|
760 usage = '%prog [OPTIONS]' |
|
761 summary = 'Output all currently installed packages (exact versions) to stdout' |
|
762 |
|
763 def __init__(self): |
|
764 super(FreezeCommand, self).__init__() |
|
765 self.parser.add_option( |
|
766 '-r', '--requirement', |
|
767 dest='requirement', |
|
768 action='store', |
|
769 default=None, |
|
770 metavar='FILENAME', |
|
771 help='Use the given requirements file as a hint about how to generate the new frozen requirements') |
|
772 self.parser.add_option( |
|
773 '-f', '--find-links', |
|
774 dest='find_links', |
|
775 action='append', |
|
776 default=[], |
|
777 metavar='URL', |
|
778 help='URL for finding packages, which will be added to the frozen requirements file') |
|
779 |
|
780 def run(self, options, args): |
|
781 requirement = options.requirement |
|
782 find_links = options.find_links or [] |
|
783 ## FIXME: Obviously this should be settable: |
|
784 find_tags = False |
|
785 skip_match = None |
|
786 |
|
787 skip_regex = options.skip_requirements_regex |
|
788 if skip_regex: |
|
789 skip_match = re.compile(skip_regex) |
|
790 |
|
791 logger.move_stdout_to_stderr() |
|
792 dependency_links = [] |
|
793 |
|
794 f = sys.stdout |
|
795 |
|
796 for dist in pkg_resources.working_set: |
|
797 if dist.has_metadata('dependency_links.txt'): |
|
798 dependency_links.extend(dist.get_metadata_lines('dependency_links.txt')) |
|
799 for link in find_links: |
|
800 if '#egg=' in link: |
|
801 dependency_links.append(link) |
|
802 for link in find_links: |
|
803 f.write('-f %s\n' % link) |
|
804 installations = {} |
|
805 for dist in pkg_resources.working_set: |
|
806 if dist.key in ('setuptools', 'pip', 'python'): |
|
807 ## FIXME: also skip virtualenv? |
|
808 continue |
|
809 req = FrozenRequirement.from_dist(dist, dependency_links, find_tags=find_tags) |
|
810 installations[req.name] = req |
|
811 if requirement: |
|
812 req_f = open(requirement) |
|
813 for line in req_f: |
|
814 if not line.strip() or line.strip().startswith('#'): |
|
815 f.write(line) |
|
816 continue |
|
817 if skip_match and skip_match.search(line): |
|
818 f.write(line) |
|
819 continue |
|
820 elif line.startswith('-e') or line.startswith('--editable'): |
|
821 if line.startswith('-e'): |
|
822 line = line[2:].strip() |
|
823 else: |
|
824 line = line[len('--editable'):].strip().lstrip('=') |
|
825 line_req = InstallRequirement.from_editable(line, default_vcs=options.default_vcs) |
|
826 elif (line.startswith('-r') or line.startswith('--requirement') |
|
827 or line.startswith('-Z') or line.startswith('--always-unzip')): |
|
828 logger.debug('Skipping line %r' % line.strip()) |
|
829 continue |
|
830 else: |
|
831 line_req = InstallRequirement.from_line(line) |
|
832 if not line_req.name: |
|
833 logger.notify("Skipping line because it's not clear what it would install: %s" |
|
834 % line.strip()) |
|
835 logger.notify(" (add #egg=PackageName to the URL to avoid this warning)") |
|
836 continue |
|
837 if line_req.name not in installations: |
|
838 logger.warn("Requirement file contains %s, but that package is not installed" |
|
839 % line.strip()) |
|
840 continue |
|
841 f.write(str(installations[line_req.name])) |
|
842 del installations[line_req.name] |
|
843 f.write('## The following requirements were added by pip --freeze:\n') |
|
844 for installation in sorted(installations.values(), key=lambda x: x.name): |
|
845 f.write(str(installation)) |
|
846 |
|
847 FreezeCommand() |
|
848 |
|
849 class ZipCommand(Command): |
|
850 name = 'zip' |
|
851 usage = '%prog [OPTIONS] PACKAGE_NAMES...' |
|
852 summary = 'Zip individual packages' |
|
853 |
|
854 def __init__(self): |
|
855 super(ZipCommand, self).__init__() |
|
856 if self.name == 'zip': |
|
857 self.parser.add_option( |
|
858 '--unzip', |
|
859 action='store_true', |
|
860 dest='unzip', |
|
861 help='Unzip (rather than zip) a package') |
|
862 else: |
|
863 self.parser.add_option( |
|
864 '--zip', |
|
865 action='store_false', |
|
866 dest='unzip', |
|
867 default=True, |
|
868 help='Zip (rather than unzip) a package') |
|
869 self.parser.add_option( |
|
870 '--no-pyc', |
|
871 action='store_true', |
|
872 dest='no_pyc', |
|
873 help='Do not include .pyc files in zip files (useful on Google App Engine)') |
|
874 self.parser.add_option( |
|
875 '-l', '--list', |
|
876 action='store_true', |
|
877 dest='list', |
|
878 help='List the packages available, and their zip status') |
|
879 self.parser.add_option( |
|
880 '--sort-files', |
|
881 action='store_true', |
|
882 dest='sort_files', |
|
883 help='With --list, sort packages according to how many files they contain') |
|
884 self.parser.add_option( |
|
885 '--path', |
|
886 action='append', |
|
887 dest='paths', |
|
888 help='Restrict operations to the given paths (may include wildcards)') |
|
889 self.parser.add_option( |
|
890 '-n', '--simulate', |
|
891 action='store_true', |
|
892 help='Do not actually perform the zip/unzip operation') |
|
893 |
|
894 def paths(self): |
|
895 """All the entries of sys.path, possibly restricted by --path""" |
|
896 if not self.select_paths: |
|
897 return sys.path |
|
898 result = [] |
|
899 match_any = set() |
|
900 for path in sys.path: |
|
901 path = os.path.normcase(os.path.abspath(path)) |
|
902 for match in self.select_paths: |
|
903 match = os.path.normcase(os.path.abspath(match)) |
|
904 if '*' in match: |
|
905 if re.search(fnmatch.translate(match+'*'), path): |
|
906 result.append(path) |
|
907 match_any.add(match) |
|
908 break |
|
909 else: |
|
910 if path.startswith(match): |
|
911 result.append(path) |
|
912 match_any.add(match) |
|
913 break |
|
914 else: |
|
915 logger.debug("Skipping path %s because it doesn't match %s" |
|
916 % (path, ', '.join(self.select_paths))) |
|
917 for match in self.select_paths: |
|
918 if match not in match_any and '*' not in match: |
|
919 result.append(match) |
|
920 logger.debug("Adding path %s because it doesn't match anything already on sys.path" |
|
921 % match) |
|
922 return result |
|
923 |
|
924 def run(self, options, args): |
|
925 self.select_paths = options.paths |
|
926 self.simulate = options.simulate |
|
927 if options.list: |
|
928 return self.list(options, args) |
|
929 if not args: |
|
930 raise InstallationError( |
|
931 'You must give at least one package to zip or unzip') |
|
932 packages = [] |
|
933 for arg in args: |
|
934 module_name, filename = self.find_package(arg) |
|
935 if options.unzip and os.path.isdir(filename): |
|
936 raise InstallationError( |
|
937 'The module %s (in %s) is not a zip file; cannot be unzipped' |
|
938 % (module_name, filename)) |
|
939 elif not options.unzip and not os.path.isdir(filename): |
|
940 raise InstallationError( |
|
941 'The module %s (in %s) is not a directory; cannot be zipped' |
|
942 % (module_name, filename)) |
|
943 packages.append((module_name, filename)) |
|
944 last_status = None |
|
945 for module_name, filename in packages: |
|
946 if options.unzip: |
|
947 last_status = self.unzip_package(module_name, filename) |
|
948 else: |
|
949 last_status = self.zip_package(module_name, filename, options.no_pyc) |
|
950 return last_status |
|
951 |
|
952 def unzip_package(self, module_name, filename): |
|
953 zip_filename = os.path.dirname(filename) |
|
954 if not os.path.isfile(zip_filename) and zipfile.is_zipfile(zip_filename): |
|
955 raise InstallationError( |
|
956 'Module %s (in %s) isn\'t located in a zip file in %s' |
|
957 % (module_name, filename, zip_filename)) |
|
958 package_path = os.path.dirname(zip_filename) |
|
959 if not package_path in self.paths(): |
|
960 logger.warn( |
|
961 'Unpacking %s into %s, but %s is not on sys.path' |
|
962 % (display_path(zip_filename), display_path(package_path), |
|
963 display_path(package_path))) |
|
964 logger.notify('Unzipping %s (in %s)' % (module_name, display_path(zip_filename))) |
|
965 if self.simulate: |
|
966 logger.notify('Skipping remaining operations because of --simulate') |
|
967 return |
|
968 logger.indent += 2 |
|
969 try: |
|
970 ## FIXME: this should be undoable: |
|
971 zip = zipfile.ZipFile(zip_filename) |
|
972 to_save = [] |
|
973 for name in zip.namelist(): |
|
974 if name.startswith('%s/' % module_name): |
|
975 content = zip.read(name) |
|
976 dest = os.path.join(package_path, name) |
|
977 if not os.path.exists(os.path.dirname(dest)): |
|
978 os.makedirs(os.path.dirname(dest)) |
|
979 if not content and dest.endswith('/'): |
|
980 if not os.path.exists(dest): |
|
981 os.makedirs(dest) |
|
982 else: |
|
983 f = open(dest, 'wb') |
|
984 f.write(content) |
|
985 f.close() |
|
986 else: |
|
987 to_save.append((name, zip.read(name))) |
|
988 zip.close() |
|
989 if not to_save: |
|
990 logger.info('Removing now-empty zip file %s' % display_path(zip_filename)) |
|
991 os.unlink(zip_filename) |
|
992 self.remove_filename_from_pth(zip_filename) |
|
993 else: |
|
994 logger.info('Removing entries in %s/ from zip file %s' % (module_name, display_path(zip_filename))) |
|
995 zip = zipfile.ZipFile(zip_filename, 'w') |
|
996 for name, content in to_save: |
|
997 zip.writestr(name, content) |
|
998 zip.close() |
|
999 finally: |
|
1000 logger.indent -= 2 |
|
1001 |
|
1002 def zip_package(self, module_name, filename, no_pyc): |
|
1003 orig_filename = filename |
|
1004 logger.notify('Zip %s (in %s)' % (module_name, display_path(filename))) |
|
1005 logger.indent += 2 |
|
1006 if filename.endswith('.egg'): |
|
1007 dest_filename = filename |
|
1008 else: |
|
1009 dest_filename = filename + '.zip' |
|
1010 try: |
|
1011 ## FIXME: I think this needs to be undoable: |
|
1012 if filename == dest_filename: |
|
1013 filename = backup_dir(orig_filename) |
|
1014 logger.notify('Moving %s aside to %s' % (orig_filename, filename)) |
|
1015 if not self.simulate: |
|
1016 shutil.move(orig_filename, filename) |
|
1017 try: |
|
1018 logger.info('Creating zip file in %s' % display_path(dest_filename)) |
|
1019 if not self.simulate: |
|
1020 zip = zipfile.ZipFile(dest_filename, 'w') |
|
1021 zip.writestr(module_name + '/', '') |
|
1022 for dirpath, dirnames, filenames in os.walk(filename): |
|
1023 if no_pyc: |
|
1024 filenames = [f for f in filenames |
|
1025 if not f.lower().endswith('.pyc')] |
|
1026 for fns, is_dir in [(dirnames, True), (filenames, False)]: |
|
1027 for fn in fns: |
|
1028 full = os.path.join(dirpath, fn) |
|
1029 dest = os.path.join(module_name, dirpath[len(filename):].lstrip(os.path.sep), fn) |
|
1030 if is_dir: |
|
1031 zip.writestr(dest+'/', '') |
|
1032 else: |
|
1033 zip.write(full, dest) |
|
1034 zip.close() |
|
1035 logger.info('Removing old directory %s' % display_path(filename)) |
|
1036 if not self.simulate: |
|
1037 shutil.rmtree(filename) |
|
1038 except: |
|
1039 ## FIXME: need to do an undo here |
|
1040 raise |
|
1041 ## FIXME: should also be undone: |
|
1042 self.add_filename_to_pth(dest_filename) |
|
1043 finally: |
|
1044 logger.indent -= 2 |
|
1045 |
|
1046 def remove_filename_from_pth(self, filename): |
|
1047 for pth in self.pth_files(): |
|
1048 f = open(pth, 'r') |
|
1049 lines = f.readlines() |
|
1050 f.close() |
|
1051 new_lines = [ |
|
1052 l for l in lines if l.strip() != filename] |
|
1053 if lines != new_lines: |
|
1054 logger.info('Removing reference to %s from .pth file %s' |
|
1055 % (display_path(filename), display_path(pth))) |
|
1056 if not filter(None, new_lines): |
|
1057 logger.info('%s file would be empty: deleting' % display_path(pth)) |
|
1058 if not self.simulate: |
|
1059 os.unlink(pth) |
|
1060 else: |
|
1061 if not self.simulate: |
|
1062 f = open(pth, 'w') |
|
1063 f.writelines(new_lines) |
|
1064 f.close() |
|
1065 return |
|
1066 logger.warn('Cannot find a reference to %s in any .pth file' % display_path(filename)) |
|
1067 |
|
1068 def add_filename_to_pth(self, filename): |
|
1069 path = os.path.dirname(filename) |
|
1070 dest = os.path.join(path, filename + '.pth') |
|
1071 if path not in self.paths(): |
|
1072 logger.warn('Adding .pth file %s, but it is not on sys.path' % display_path(dest)) |
|
1073 if not self.simulate: |
|
1074 if os.path.exists(dest): |
|
1075 f = open(dest) |
|
1076 lines = f.readlines() |
|
1077 f.close() |
|
1078 if lines and not lines[-1].endswith('\n'): |
|
1079 lines[-1] += '\n' |
|
1080 lines.append(filename+'\n') |
|
1081 else: |
|
1082 lines = [filename + '\n'] |
|
1083 f = open(dest, 'w') |
|
1084 f.writelines(lines) |
|
1085 f.close() |
|
1086 |
|
1087 def pth_files(self): |
|
1088 for path in self.paths(): |
|
1089 if not os.path.exists(path) or not os.path.isdir(path): |
|
1090 continue |
|
1091 for filename in os.listdir(path): |
|
1092 if filename.endswith('.pth'): |
|
1093 yield os.path.join(path, filename) |
|
1094 |
|
1095 def find_package(self, package): |
|
1096 for path in self.paths(): |
|
1097 full = os.path.join(path, package) |
|
1098 if os.path.exists(full): |
|
1099 return package, full |
|
1100 if not os.path.isdir(path) and zipfile.is_zipfile(path): |
|
1101 zip = zipfile.ZipFile(path, 'r') |
|
1102 try: |
|
1103 zip.read('%s/__init__.py' % package) |
|
1104 except KeyError: |
|
1105 pass |
|
1106 else: |
|
1107 zip.close() |
|
1108 return package, full |
|
1109 zip.close() |
|
1110 ## FIXME: need special error for package.py case: |
|
1111 raise InstallationError( |
|
1112 'No package with the name %s found' % package) |
|
1113 |
|
1114 def list(self, options, args): |
|
1115 if args: |
|
1116 raise InstallationError( |
|
1117 'You cannot give an argument with --list') |
|
1118 for path in sorted(self.paths()): |
|
1119 if not os.path.exists(path): |
|
1120 continue |
|
1121 basename = os.path.basename(path.rstrip(os.path.sep)) |
|
1122 if os.path.isfile(path) and zipfile.is_zipfile(path): |
|
1123 if os.path.dirname(path) not in self.paths(): |
|
1124 logger.notify('Zipped egg: %s' % display_path(path)) |
|
1125 continue |
|
1126 if (basename != 'site-packages' |
|
1127 and not path.replace('\\', '/').endswith('lib/python')): |
|
1128 continue |
|
1129 logger.notify('In %s:' % display_path(path)) |
|
1130 logger.indent += 2 |
|
1131 zipped = [] |
|
1132 unzipped = [] |
|
1133 try: |
|
1134 for filename in sorted(os.listdir(path)): |
|
1135 ext = os.path.splitext(filename)[1].lower() |
|
1136 if ext in ('.pth', '.egg-info', '.egg-link'): |
|
1137 continue |
|
1138 if ext == '.py': |
|
1139 logger.info('Not displaying %s: not a package' % display_path(filename)) |
|
1140 continue |
|
1141 full = os.path.join(path, filename) |
|
1142 if os.path.isdir(full): |
|
1143 unzipped.append((filename, self.count_package(full))) |
|
1144 elif zipfile.is_zipfile(full): |
|
1145 zipped.append(filename) |
|
1146 else: |
|
1147 logger.info('Unknown file: %s' % display_path(filename)) |
|
1148 if zipped: |
|
1149 logger.notify('Zipped packages:') |
|
1150 logger.indent += 2 |
|
1151 try: |
|
1152 for filename in zipped: |
|
1153 logger.notify(filename) |
|
1154 finally: |
|
1155 logger.indent -= 2 |
|
1156 else: |
|
1157 logger.notify('No zipped packages.') |
|
1158 if unzipped: |
|
1159 if options.sort_files: |
|
1160 unzipped.sort(key=lambda x: -x[1]) |
|
1161 logger.notify('Unzipped packages:') |
|
1162 logger.indent += 2 |
|
1163 try: |
|
1164 for filename, count in unzipped: |
|
1165 logger.notify('%s (%i files)' % (filename, count)) |
|
1166 finally: |
|
1167 logger.indent -= 2 |
|
1168 else: |
|
1169 logger.notify('No unzipped packages.') |
|
1170 finally: |
|
1171 logger.indent -= 2 |
|
1172 |
|
1173 def count_package(self, path): |
|
1174 total = 0 |
|
1175 for dirpath, dirnames, filenames in os.walk(path): |
|
1176 filenames = [f for f in filenames |
|
1177 if not f.lower().endswith('.pyc')] |
|
1178 total += len(filenames) |
|
1179 return total |
|
1180 |
|
1181 ZipCommand() |
|
1182 |
|
1183 class UnzipCommand(ZipCommand): |
|
1184 name = 'unzip' |
|
1185 summary = 'Unzip individual packages' |
|
1186 |
|
1187 UnzipCommand() |
|
1188 |
|
1189 BASE_COMPLETION = """ |
|
1190 # pip %(shell)s completion start%(script)s# pip %(shell)s completion end |
|
1191 """ |
|
1192 |
|
1193 COMPLETION_SCRIPTS = { |
|
1194 'bash': """ |
|
1195 _pip_completion() |
|
1196 { |
|
1197 COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\ |
|
1198 COMP_CWORD=$COMP_CWORD \\ |
|
1199 PIP_AUTO_COMPLETE=1 $1 ) ) |
|
1200 } |
|
1201 complete -o default -F _pip_completion pip |
|
1202 """, 'zsh': """ |
|
1203 function _pip_completion { |
|
1204 local words cword |
|
1205 read -Ac words |
|
1206 read -cn cword |
|
1207 reply=( $( COMP_WORDS="$words[*]" \\ |
|
1208 COMP_CWORD=$(( cword-1 )) \\ |
|
1209 PIP_AUTO_COMPLETE=1 $words[1] ) ) |
|
1210 } |
|
1211 compctl -K _pip_completion pip |
|
1212 """ |
|
1213 } |
|
1214 |
|
1215 class CompletionCommand(Command): |
|
1216 name = 'completion' |
|
1217 summary = 'A helper command to be used for command completion' |
|
1218 hidden = True |
|
1219 |
|
1220 def __init__(self): |
|
1221 super(CompletionCommand, self).__init__() |
|
1222 self.parser.add_option( |
|
1223 '--bash', '-b', |
|
1224 action='store_const', |
|
1225 const='bash', |
|
1226 dest='shell', |
|
1227 help='Emit completion code for bash') |
|
1228 self.parser.add_option( |
|
1229 '--zsh', '-z', |
|
1230 action='store_const', |
|
1231 const='zsh', |
|
1232 dest='shell', |
|
1233 help='Emit completion code for zsh') |
|
1234 |
|
1235 def run(self, options, args): |
|
1236 """Prints the completion code of the given shell""" |
|
1237 if options.shell in ('bash', 'zsh'): |
|
1238 script = COMPLETION_SCRIPTS.get(options.shell, '') |
|
1239 print BASE_COMPLETION % {'script': script, 'shell': options.shell} |
|
1240 else: |
|
1241 print 'ERROR: You must pass --bash or --zsh' |
|
1242 |
|
1243 CompletionCommand() |
|
1244 |
|
1245 def autocomplete(): |
|
1246 """Command and option completion for the main option parser (and options) |
|
1247 and its subcommands (and options). |
|
1248 |
|
1249 Enable by sourcing one of the completion shell scripts (bash or zsh). |
|
1250 """ |
|
1251 # Don't complete if user hasn't sourced bash_completion file. |
|
1252 if not os.environ.has_key('PIP_AUTO_COMPLETE'): |
|
1253 return |
|
1254 cwords = os.environ['COMP_WORDS'].split()[1:] |
|
1255 cword = int(os.environ['COMP_CWORD']) |
|
1256 try: |
|
1257 current = cwords[cword-1] |
|
1258 except IndexError: |
|
1259 current = '' |
|
1260 subcommands = [cmd for cmd, cls in _commands.items() if not cls.hidden] |
|
1261 options = [] |
|
1262 # subcommand |
|
1263 if cword == 1: |
|
1264 # show options of main parser only when necessary |
|
1265 if current.startswith('-') or current.startswith('--'): |
|
1266 subcommands += [opt.get_opt_string() |
|
1267 for opt in parser.option_list |
|
1268 if opt.help != optparse.SUPPRESS_HELP] |
|
1269 print ' '.join(filter(lambda x: x.startswith(current), subcommands)) |
|
1270 # subcommand options |
|
1271 # special case: the 'help' subcommand has no options |
|
1272 elif cwords[0] in subcommands and cwords[0] != 'help': |
|
1273 subcommand = _commands.get(cwords[0]) |
|
1274 options += [(opt.get_opt_string(), opt.nargs) |
|
1275 for opt in subcommand.parser.option_list |
|
1276 if opt.help != optparse.SUPPRESS_HELP] |
|
1277 # filter out previously specified options from available options |
|
1278 prev_opts = [x.split('=')[0] for x in cwords[1:cword-1]] |
|
1279 options = filter(lambda (x, v): x not in prev_opts, options) |
|
1280 # filter options by current input |
|
1281 options = [(k, v) for k, v in options if k.startswith(current)] |
|
1282 for option in options: |
|
1283 opt_label = option[0] |
|
1284 # append '=' to options which require args |
|
1285 if option[1]: |
|
1286 opt_label += '=' |
|
1287 print opt_label |
|
1288 sys.exit(1) |
|
1289 |
|
1290 def main(initial_args=None): |
|
1291 if initial_args is None: |
|
1292 initial_args = sys.argv[1:] |
|
1293 autocomplete() |
|
1294 options, args = parser.parse_args(initial_args) |
|
1295 if options.help and not args: |
|
1296 args = ['help'] |
|
1297 if not args: |
|
1298 parser.error('You must give a command (use "pip help" see a list of commands)') |
|
1299 command = args[0].lower() |
|
1300 ## FIXME: search for a command match? |
|
1301 if command not in _commands: |
|
1302 parser.error('No command by the name %(script)s %(arg)s\n (maybe you meant "%(script)s install %(arg)s")' |
|
1303 % dict(script=os.path.basename(sys.argv[0]), arg=command)) |
|
1304 command = _commands[command] |
|
1305 return command.main(initial_args, args[1:], options) |
|
1306 |
|
1307 def get_proxy(proxystr=''): |
|
1308 """Get the proxy given the option passed on the command line. If an |
|
1309 empty string is passed it looks at the HTTP_PROXY environment |
|
1310 variable.""" |
|
1311 if not proxystr: |
|
1312 proxystr = os.environ.get('HTTP_PROXY', '') |
|
1313 if proxystr: |
|
1314 if '@' in proxystr: |
|
1315 user_password, server_port = proxystr.split('@', 1) |
|
1316 if ':' in user_password: |
|
1317 user, password = user_password.split(':', 1) |
|
1318 else: |
|
1319 user = user_password |
|
1320 import getpass |
|
1321 prompt = 'Password for %s@%s: ' % (user, server_port) |
|
1322 password = urllib.quote(getpass.getpass(prompt)) |
|
1323 return '%s:%s@%s' % (user, password, server_port) |
|
1324 else: |
|
1325 return proxystr |
|
1326 else: |
|
1327 return None |
|
1328 |
|
1329 def setup_proxy_handler(proxystr=''): |
|
1330 """Set the proxy handler given the option passed on the command |
|
1331 line. If an empty string is passed it looks at the HTTP_PROXY |
|
1332 environment variable. """ |
|
1333 proxy = get_proxy(proxystr) |
|
1334 if proxy: |
|
1335 proxy_support = urllib2.ProxyHandler({"http": proxy, "ftp": proxy}) |
|
1336 opener = urllib2.build_opener(proxy_support, urllib2.CacheFTPHandler) |
|
1337 urllib2.install_opener(opener) |
|
1338 |
|
1339 def format_exc(exc_info=None): |
|
1340 if exc_info is None: |
|
1341 exc_info = sys.exc_info() |
|
1342 out = StringIO() |
|
1343 traceback.print_exception(*exc_info, **dict(file=out)) |
|
1344 return out.getvalue() |
|
1345 |
|
1346 def restart_in_venv(venv, base, site_packages, args): |
|
1347 """ |
|
1348 Restart this script using the interpreter in the given virtual environment |
|
1349 """ |
|
1350 if base and not os.path.isabs(venv) and not venv.startswith('~'): |
|
1351 base = os.path.expanduser(base) |
|
1352 # ensure we have an abs basepath at this point: |
|
1353 # a relative one makes no sense (or does it?) |
|
1354 if os.path.isabs(base): |
|
1355 venv = os.path.join(base, venv) |
|
1356 |
|
1357 if venv.startswith('~'): |
|
1358 venv = os.path.expanduser(venv) |
|
1359 |
|
1360 if not os.path.exists(venv): |
|
1361 try: |
|
1362 import virtualenv |
|
1363 except ImportError: |
|
1364 print 'The virtual environment does not exist: %s' % venv |
|
1365 print 'and virtualenv is not installed, so a new environment cannot be created' |
|
1366 sys.exit(3) |
|
1367 print 'Creating new virtualenv environment in %s' % venv |
|
1368 virtualenv.logger = logger |
|
1369 logger.indent += 2 |
|
1370 virtualenv.create_environment(venv, site_packages=site_packages) |
|
1371 if sys.platform == 'win32': |
|
1372 python = os.path.join(venv, 'Scripts', 'python.exe') |
|
1373 # check for bin directory which is used in buildouts |
|
1374 if not os.path.exists(python): |
|
1375 python = os.path.join(venv, 'bin', 'python.exe') |
|
1376 else: |
|
1377 python = os.path.join(venv, 'bin', 'python') |
|
1378 if not os.path.exists(python): |
|
1379 python = venv |
|
1380 if not os.path.exists(python): |
|
1381 raise BadCommand('Cannot find virtual environment interpreter at %s' % python) |
|
1382 base = os.path.dirname(os.path.dirname(python)) |
|
1383 file = __file__ |
|
1384 if file.endswith('.pyc'): |
|
1385 file = file[:-1] |
|
1386 proc = subprocess.Popen( |
|
1387 [python, file] + args + [base, '___VENV_RESTART___']) |
|
1388 proc.wait() |
|
1389 sys.exit(proc.returncode) |
|
1390 |
|
1391 class PackageFinder(object): |
|
1392 """This finds packages. |
|
1393 |
|
1394 This is meant to match easy_install's technique for looking for |
|
1395 packages, by reading pages and looking for appropriate links |
|
1396 """ |
|
1397 |
|
1398 failure_limit = 3 |
|
1399 |
|
1400 def __init__(self, find_links, index_urls): |
|
1401 self.find_links = find_links |
|
1402 self.index_urls = index_urls |
|
1403 self.dependency_links = [] |
|
1404 self.cache = PageCache() |
|
1405 # These are boring links that have already been logged somehow: |
|
1406 self.logged_links = set() |
|
1407 |
|
1408 def add_dependency_links(self, links): |
|
1409 ## FIXME: this shouldn't be global list this, it should only |
|
1410 ## apply to requirements of the package that specifies the |
|
1411 ## dependency_links value |
|
1412 ## FIXME: also, we should track comes_from (i.e., use Link) |
|
1413 self.dependency_links.extend(links) |
|
1414 |
|
1415 def find_requirement(self, req, upgrade): |
|
1416 url_name = req.url_name |
|
1417 # Only check main index if index URL is given: |
|
1418 main_index_url = None |
|
1419 if self.index_urls: |
|
1420 # Check that we have the url_name correctly spelled: |
|
1421 main_index_url = Link(posixpath.join(self.index_urls[0], url_name)) |
|
1422 # This will also cache the page, so it's okay that we get it again later: |
|
1423 page = self._get_page(main_index_url, req) |
|
1424 if page is None: |
|
1425 url_name = self._find_url_name(Link(self.index_urls[0]), url_name, req) or req.url_name |
|
1426 def mkurl_pypi_url(url): |
|
1427 loc = posixpath.join(url, url_name) |
|
1428 # For maximum compatibility with easy_install, ensure the path |
|
1429 # ends in a trailing slash. Although this isn't in the spec |
|
1430 # (and PyPI can handle it without the slash) some other index |
|
1431 # implementations might break if they relied on easy_install's behavior. |
|
1432 if not loc.endswith('/'): |
|
1433 loc = loc + '/' |
|
1434 return loc |
|
1435 if url_name is not None: |
|
1436 locations = [ |
|
1437 mkurl_pypi_url(url) |
|
1438 for url in self.index_urls] + self.find_links |
|
1439 else: |
|
1440 locations = list(self.find_links) |
|
1441 locations.extend(self.dependency_links) |
|
1442 for version in req.absolute_versions: |
|
1443 if url_name is not None and main_index_url is not None: |
|
1444 locations = [ |
|
1445 posixpath.join(main_index_url.url, version)] + locations |
|
1446 file_locations = [] |
|
1447 url_locations = [] |
|
1448 for url in locations: |
|
1449 if url.startswith('file:'): |
|
1450 fn = url_to_filename(url) |
|
1451 if os.path.isdir(fn): |
|
1452 path = os.path.realpath(fn) |
|
1453 for item in os.listdir(path): |
|
1454 file_locations.append( |
|
1455 filename_to_url2(os.path.join(path, item))) |
|
1456 elif os.path.isfile(fn): |
|
1457 file_locations.append(filename_to_url2(fn)) |
|
1458 else: |
|
1459 url_locations.append(url) |
|
1460 |
|
1461 locations = [Link(url) for url in url_locations] |
|
1462 logger.debug('URLs to search for versions for %s:' % req) |
|
1463 for location in locations: |
|
1464 logger.debug('* %s' % location) |
|
1465 found_versions = [] |
|
1466 found_versions.extend( |
|
1467 self._package_versions( |
|
1468 [Link(url, '-f') for url in self.find_links], req.name.lower())) |
|
1469 page_versions = [] |
|
1470 for page in self._get_pages(locations, req): |
|
1471 logger.debug('Analyzing links from page %s' % page.url) |
|
1472 logger.indent += 2 |
|
1473 try: |
|
1474 page_versions.extend(self._package_versions(page.links, req.name.lower())) |
|
1475 finally: |
|
1476 logger.indent -= 2 |
|
1477 dependency_versions = list(self._package_versions( |
|
1478 [Link(url) for url in self.dependency_links], req.name.lower())) |
|
1479 if dependency_versions: |
|
1480 logger.info('dependency_links found: %s' % ', '.join([link.url for parsed, link, version in dependency_versions])) |
|
1481 file_versions = list(self._package_versions( |
|
1482 [Link(url) for url in file_locations], req.name.lower())) |
|
1483 if not found_versions and not page_versions and not dependency_versions and not file_versions: |
|
1484 logger.fatal('Could not find any downloads that satisfy the requirement %s' % req) |
|
1485 raise DistributionNotFound('No distributions at all found for %s' % req) |
|
1486 if req.satisfied_by is not None: |
|
1487 found_versions.append((req.satisfied_by.parsed_version, Inf, req.satisfied_by.version)) |
|
1488 if file_versions: |
|
1489 file_versions.sort(reverse=True) |
|
1490 logger.info('Local files found: %s' % ', '.join([url_to_filename(link.url) for parsed, link, version in file_versions])) |
|
1491 found_versions = file_versions + found_versions |
|
1492 all_versions = found_versions + page_versions + dependency_versions |
|
1493 applicable_versions = [] |
|
1494 for (parsed_version, link, version) in all_versions: |
|
1495 if version not in req.req: |
|
1496 logger.info("Ignoring link %s, version %s doesn't match %s" |
|
1497 % (link, version, ','.join([''.join(s) for s in req.req.specs]))) |
|
1498 continue |
|
1499 applicable_versions.append((link, version)) |
|
1500 applicable_versions = sorted(applicable_versions, key=operator.itemgetter(1), |
|
1501 cmp=lambda x, y : cmp(pkg_resources.parse_version(y), pkg_resources.parse_version(x)) |
|
1502 ) |
|
1503 existing_applicable = bool([link for link, version in applicable_versions if link is Inf]) |
|
1504 if not upgrade and existing_applicable: |
|
1505 if applicable_versions[0][1] is Inf: |
|
1506 logger.info('Existing installed version (%s) is most up-to-date and satisfies requirement' |
|
1507 % req.satisfied_by.version) |
|
1508 else: |
|
1509 logger.info('Existing installed version (%s) satisfies requirement (most up-to-date version is %s)' |
|
1510 % (req.satisfied_by.version, applicable_versions[0][1])) |
|
1511 return None |
|
1512 if not applicable_versions: |
|
1513 logger.fatal('Could not find a version that satisfies the requirement %s (from versions: %s)' |
|
1514 % (req, ', '.join([version for parsed_version, link, version in found_versions]))) |
|
1515 raise DistributionNotFound('No distributions matching the version for %s' % req) |
|
1516 if applicable_versions[0][0] is Inf: |
|
1517 # We have an existing version, and its the best version |
|
1518 logger.info('Installed version (%s) is most up-to-date (past versions: %s)' |
|
1519 % (req.satisfied_by.version, ', '.join([version for link, version in applicable_versions[1:]]) or 'none')) |
|
1520 return None |
|
1521 if len(applicable_versions) > 1: |
|
1522 logger.info('Using version %s (newest of versions: %s)' % |
|
1523 (applicable_versions[0][1], ', '.join([version for link, version in applicable_versions]))) |
|
1524 return applicable_versions[0][0] |
|
1525 |
|
1526 def _find_url_name(self, index_url, url_name, req): |
|
1527 """Finds the true URL name of a package, when the given name isn't quite correct. |
|
1528 This is usually used to implement case-insensitivity.""" |
|
1529 if not index_url.url.endswith('/'): |
|
1530 # Vaguely part of the PyPI API... weird but true. |
|
1531 ## FIXME: bad to modify this? |
|
1532 index_url.url += '/' |
|
1533 page = self._get_page(index_url, req) |
|
1534 if page is None: |
|
1535 logger.fatal('Cannot fetch index base URL %s' % index_url) |
|
1536 return |
|
1537 norm_name = normalize_name(req.url_name) |
|
1538 for link in page.links: |
|
1539 base = posixpath.basename(link.path.rstrip('/')) |
|
1540 if norm_name == normalize_name(base): |
|
1541 logger.notify('Real name of requirement %s is %s' % (url_name, base)) |
|
1542 return base |
|
1543 return None |
|
1544 |
|
1545 def _get_pages(self, locations, req): |
|
1546 """Yields (page, page_url) from the given locations, skipping |
|
1547 locations that have errors, and adding download/homepage links""" |
|
1548 pending_queue = Queue() |
|
1549 for location in locations: |
|
1550 pending_queue.put(location) |
|
1551 done = [] |
|
1552 seen = set() |
|
1553 threads = [] |
|
1554 for i in range(min(10, len(locations))): |
|
1555 t = threading.Thread(target=self._get_queued_page, args=(req, pending_queue, done, seen)) |
|
1556 t.setDaemon(True) |
|
1557 threads.append(t) |
|
1558 t.start() |
|
1559 for t in threads: |
|
1560 t.join() |
|
1561 return done |
|
1562 |
|
1563 _log_lock = threading.Lock() |
|
1564 |
|
1565 def _get_queued_page(self, req, pending_queue, done, seen): |
|
1566 while 1: |
|
1567 try: |
|
1568 location = pending_queue.get(False) |
|
1569 except QueueEmpty: |
|
1570 return |
|
1571 if location in seen: |
|
1572 continue |
|
1573 seen.add(location) |
|
1574 page = self._get_page(location, req) |
|
1575 if page is None: |
|
1576 continue |
|
1577 done.append(page) |
|
1578 for link in page.rel_links(): |
|
1579 pending_queue.put(link) |
|
1580 |
|
1581 _egg_fragment_re = re.compile(r'#egg=([^&]*)') |
|
1582 _egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.-]+)', re.I) |
|
1583 _py_version_re = re.compile(r'-py([123]\.[0-9])$') |
|
1584 |
|
1585 def _sort_links(self, links): |
|
1586 "Brings links in order, non-egg links first, egg links second" |
|
1587 eggs, no_eggs = [], [] |
|
1588 for link in links: |
|
1589 if link.egg_fragment: |
|
1590 eggs.append(link) |
|
1591 else: |
|
1592 no_eggs.append(link) |
|
1593 return no_eggs + eggs |
|
1594 |
|
1595 def _package_versions(self, links, search_name): |
|
1596 seen_links = {} |
|
1597 for link in self._sort_links(links): |
|
1598 if link.url in seen_links: |
|
1599 continue |
|
1600 seen_links[link.url] = None |
|
1601 if link.egg_fragment: |
|
1602 egg_info = link.egg_fragment |
|
1603 else: |
|
1604 path = link.path |
|
1605 egg_info, ext = link.splitext() |
|
1606 if not ext: |
|
1607 if link not in self.logged_links: |
|
1608 logger.debug('Skipping link %s; not a file' % link) |
|
1609 self.logged_links.add(link) |
|
1610 continue |
|
1611 if egg_info.endswith('.tar'): |
|
1612 # Special double-extension case: |
|
1613 egg_info = egg_info[:-4] |
|
1614 ext = '.tar' + ext |
|
1615 if ext not in ('.tar.gz', '.tar.bz2', '.tar', '.tgz', '.zip'): |
|
1616 if link not in self.logged_links: |
|
1617 logger.debug('Skipping link %s; unknown archive format: %s' % (link, ext)) |
|
1618 self.logged_links.add(link) |
|
1619 continue |
|
1620 version = self._egg_info_matches(egg_info, search_name, link) |
|
1621 if version is None: |
|
1622 logger.debug('Skipping link %s; wrong project name (not %s)' % (link, search_name)) |
|
1623 continue |
|
1624 match = self._py_version_re.search(version) |
|
1625 if match: |
|
1626 version = version[:match.start()] |
|
1627 py_version = match.group(1) |
|
1628 if py_version != sys.version[:3]: |
|
1629 logger.debug('Skipping %s because Python version is incorrect' % link) |
|
1630 continue |
|
1631 logger.debug('Found link %s, version: %s' % (link, version)) |
|
1632 yield (pkg_resources.parse_version(version), |
|
1633 link, |
|
1634 version) |
|
1635 |
|
1636 def _egg_info_matches(self, egg_info, search_name, link): |
|
1637 match = self._egg_info_re.search(egg_info) |
|
1638 if not match: |
|
1639 logger.debug('Could not parse version from link: %s' % link) |
|
1640 return None |
|
1641 name = match.group(0).lower() |
|
1642 # To match the "safe" name that pkg_resources creates: |
|
1643 name = name.replace('_', '-') |
|
1644 if name.startswith(search_name.lower()): |
|
1645 return match.group(0)[len(search_name):].lstrip('-') |
|
1646 else: |
|
1647 return None |
|
1648 |
|
1649 def _get_page(self, link, req): |
|
1650 return HTMLPage.get_page(link, req, cache=self.cache) |
|
1651 |
|
1652 |
|
1653 class InstallRequirement(object): |
|
1654 |
|
1655 def __init__(self, req, comes_from, source_dir=None, editable=False, |
|
1656 url=None, update=True): |
|
1657 if isinstance(req, basestring): |
|
1658 req = pkg_resources.Requirement.parse(req) |
|
1659 self.req = req |
|
1660 self.comes_from = comes_from |
|
1661 self.source_dir = source_dir |
|
1662 self.editable = editable |
|
1663 self.url = url |
|
1664 self._egg_info_path = None |
|
1665 # This holds the pkg_resources.Distribution object if this requirement |
|
1666 # is already available: |
|
1667 self.satisfied_by = None |
|
1668 # This hold the pkg_resources.Distribution object if this requirement |
|
1669 # conflicts with another installed distribution: |
|
1670 self.conflicts_with = None |
|
1671 self._temp_build_dir = None |
|
1672 self._is_bundle = None |
|
1673 # True if the editable should be updated: |
|
1674 self.update = update |
|
1675 # Set to True after successful installation |
|
1676 self.install_succeeded = None |
|
1677 # UninstallPathSet of uninstalled distribution (for possible rollback) |
|
1678 self.uninstalled = None |
|
1679 |
|
1680 @classmethod |
|
1681 def from_editable(cls, editable_req, comes_from=None, default_vcs=None): |
|
1682 name, url = parse_editable(editable_req, default_vcs) |
|
1683 if url.startswith('file:'): |
|
1684 source_dir = url_to_filename(url) |
|
1685 else: |
|
1686 source_dir = None |
|
1687 return cls(name, comes_from, source_dir=source_dir, editable=True, url=url) |
|
1688 |
|
1689 @classmethod |
|
1690 def from_line(cls, name, comes_from=None): |
|
1691 """Creates an InstallRequirement from a name, which might be a |
|
1692 requirement, filename, or URL. |
|
1693 """ |
|
1694 url = None |
|
1695 name = name.strip() |
|
1696 req = name |
|
1697 if is_url(name): |
|
1698 url = name |
|
1699 ## FIXME: I think getting the requirement here is a bad idea: |
|
1700 #req = get_requirement_from_url(url) |
|
1701 req = None |
|
1702 elif is_filename(name): |
|
1703 if not os.path.exists(name): |
|
1704 logger.warn('Requirement %r looks like a filename, but the file does not exist' |
|
1705 % name) |
|
1706 url = filename_to_url(name) |
|
1707 #req = get_requirement_from_url(url) |
|
1708 req = None |
|
1709 return cls(req, comes_from, url=url) |
|
1710 |
|
1711 def __str__(self): |
|
1712 if self.req: |
|
1713 s = str(self.req) |
|
1714 if self.url: |
|
1715 s += ' from %s' % self.url |
|
1716 else: |
|
1717 s = self.url |
|
1718 if self.satisfied_by is not None: |
|
1719 s += ' in %s' % display_path(self.satisfied_by.location) |
|
1720 if self.comes_from: |
|
1721 if isinstance(self.comes_from, basestring): |
|
1722 comes_from = self.comes_from |
|
1723 else: |
|
1724 comes_from = self.comes_from.from_path() |
|
1725 if comes_from: |
|
1726 s += ' (from %s)' % comes_from |
|
1727 return s |
|
1728 |
|
1729 def from_path(self): |
|
1730 if self.req is None: |
|
1731 return None |
|
1732 s = str(self.req) |
|
1733 if self.comes_from: |
|
1734 if isinstance(self.comes_from, basestring): |
|
1735 comes_from = self.comes_from |
|
1736 else: |
|
1737 comes_from = self.comes_from.from_path() |
|
1738 if comes_from: |
|
1739 s += '->' + comes_from |
|
1740 return s |
|
1741 |
|
1742 def build_location(self, build_dir, unpack=True): |
|
1743 if self._temp_build_dir is not None: |
|
1744 return self._temp_build_dir |
|
1745 if self.req is None: |
|
1746 self._temp_build_dir = tempfile.mkdtemp('-build', 'pip-') |
|
1747 self._ideal_build_dir = build_dir |
|
1748 return self._temp_build_dir |
|
1749 if self.editable: |
|
1750 name = self.name.lower() |
|
1751 else: |
|
1752 name = self.name |
|
1753 # FIXME: Is there a better place to create the build_dir? (hg and bzr need this) |
|
1754 if not os.path.exists(build_dir): |
|
1755 os.makedirs(build_dir) |
|
1756 return os.path.join(build_dir, name) |
|
1757 |
|
1758 def correct_build_location(self): |
|
1759 """If the build location was a temporary directory, this will move it |
|
1760 to a new more permanent location""" |
|
1761 if self.source_dir is not None: |
|
1762 return |
|
1763 assert self.req is not None |
|
1764 assert self._temp_build_dir |
|
1765 old_location = self._temp_build_dir |
|
1766 new_build_dir = self._ideal_build_dir |
|
1767 del self._ideal_build_dir |
|
1768 if self.editable: |
|
1769 name = self.name.lower() |
|
1770 else: |
|
1771 name = self.name |
|
1772 new_location = os.path.join(new_build_dir, name) |
|
1773 if not os.path.exists(new_build_dir): |
|
1774 logger.debug('Creating directory %s' % new_build_dir) |
|
1775 os.makedirs(new_build_dir) |
|
1776 if os.path.exists(new_location): |
|
1777 raise InstallationError( |
|
1778 'A package already exists in %s; please remove it to continue' |
|
1779 % display_path(new_location)) |
|
1780 logger.debug('Moving package %s from %s to new location %s' |
|
1781 % (self, display_path(old_location), display_path(new_location))) |
|
1782 shutil.move(old_location, new_location) |
|
1783 self._temp_build_dir = new_location |
|
1784 self.source_dir = new_location |
|
1785 self._egg_info_path = None |
|
1786 |
|
1787 @property |
|
1788 def name(self): |
|
1789 if self.req is None: |
|
1790 return None |
|
1791 return self.req.project_name |
|
1792 |
|
1793 @property |
|
1794 def url_name(self): |
|
1795 if self.req is None: |
|
1796 return None |
|
1797 return urllib.quote(self.req.unsafe_name) |
|
1798 |
|
1799 @property |
|
1800 def setup_py(self): |
|
1801 return os.path.join(self.source_dir, 'setup.py') |
|
1802 |
|
1803 def run_egg_info(self, force_root_egg_info=False): |
|
1804 assert self.source_dir |
|
1805 if self.name: |
|
1806 logger.notify('Running setup.py egg_info for package %s' % self.name) |
|
1807 else: |
|
1808 logger.notify('Running setup.py egg_info for package from %s' % self.url) |
|
1809 logger.indent += 2 |
|
1810 try: |
|
1811 script = self._run_setup_py |
|
1812 script = script.replace('__SETUP_PY__', repr(self.setup_py)) |
|
1813 script = script.replace('__PKG_NAME__', repr(self.name)) |
|
1814 # We can't put the .egg-info files at the root, because then the source code will be mistaken |
|
1815 # for an installed egg, causing problems |
|
1816 if self.editable or force_root_egg_info: |
|
1817 egg_base_option = [] |
|
1818 else: |
|
1819 egg_info_dir = os.path.join(self.source_dir, 'pip-egg-info') |
|
1820 if not os.path.exists(egg_info_dir): |
|
1821 os.makedirs(egg_info_dir) |
|
1822 egg_base_option = ['--egg-base', 'pip-egg-info'] |
|
1823 call_subprocess( |
|
1824 [sys.executable, '-c', script, 'egg_info'] + egg_base_option, |
|
1825 cwd=self.source_dir, filter_stdout=self._filter_install, show_stdout=False, |
|
1826 command_level=Logger.VERBOSE_DEBUG, |
|
1827 command_desc='python setup.py egg_info') |
|
1828 finally: |
|
1829 logger.indent -= 2 |
|
1830 if not self.req: |
|
1831 self.req = pkg_resources.Requirement.parse(self.pkg_info()['Name']) |
|
1832 self.correct_build_location() |
|
1833 |
|
1834 ## FIXME: This is a lame hack, entirely for PasteScript which has |
|
1835 ## a self-provided entry point that causes this awkwardness |
|
1836 _run_setup_py = """ |
|
1837 __file__ = __SETUP_PY__ |
|
1838 from setuptools.command import egg_info |
|
1839 def replacement_run(self): |
|
1840 self.mkpath(self.egg_info) |
|
1841 installer = self.distribution.fetch_build_egg |
|
1842 for ep in egg_info.iter_entry_points('egg_info.writers'): |
|
1843 # require=False is the change we're making: |
|
1844 writer = ep.load(require=False) |
|
1845 if writer: |
|
1846 writer(self, ep.name, egg_info.os.path.join(self.egg_info,ep.name)) |
|
1847 self.find_sources() |
|
1848 egg_info.egg_info.run = replacement_run |
|
1849 execfile(__file__) |
|
1850 """ |
|
1851 |
|
1852 def egg_info_data(self, filename): |
|
1853 if self.satisfied_by is not None: |
|
1854 if not self.satisfied_by.has_metadata(filename): |
|
1855 return None |
|
1856 return self.satisfied_by.get_metadata(filename) |
|
1857 assert self.source_dir |
|
1858 filename = self.egg_info_path(filename) |
|
1859 if not os.path.exists(filename): |
|
1860 return None |
|
1861 fp = open(filename, 'r') |
|
1862 data = fp.read() |
|
1863 fp.close() |
|
1864 return data |
|
1865 |
|
1866 def egg_info_path(self, filename): |
|
1867 if self._egg_info_path is None: |
|
1868 if self.editable: |
|
1869 base = self.source_dir |
|
1870 else: |
|
1871 base = os.path.join(self.source_dir, 'pip-egg-info') |
|
1872 filenames = os.listdir(base) |
|
1873 if self.editable: |
|
1874 filenames = [] |
|
1875 for root, dirs, files in os.walk(base): |
|
1876 for dir in vcs.dirnames: |
|
1877 if dir in dirs: |
|
1878 dirs.remove(dir) |
|
1879 filenames.extend([os.path.join(root, dir) |
|
1880 for dir in dirs]) |
|
1881 filenames = [f for f in filenames if f.endswith('.egg-info')] |
|
1882 assert filenames, "No files/directories in %s (from %s)" % (base, filename) |
|
1883 assert len(filenames) == 1, "Unexpected files/directories in %s: %s" % (base, ' '.join(filenames)) |
|
1884 self._egg_info_path = os.path.join(base, filenames[0]) |
|
1885 return os.path.join(self._egg_info_path, filename) |
|
1886 |
|
1887 def egg_info_lines(self, filename): |
|
1888 data = self.egg_info_data(filename) |
|
1889 if not data: |
|
1890 return [] |
|
1891 result = [] |
|
1892 for line in data.splitlines(): |
|
1893 line = line.strip() |
|
1894 if not line or line.startswith('#'): |
|
1895 continue |
|
1896 result.append(line) |
|
1897 return result |
|
1898 |
|
1899 def pkg_info(self): |
|
1900 p = FeedParser() |
|
1901 data = self.egg_info_data('PKG-INFO') |
|
1902 if not data: |
|
1903 logger.warn('No PKG-INFO file found in %s' % display_path(self.egg_info_path('PKG-INFO'))) |
|
1904 p.feed(data or '') |
|
1905 return p.close() |
|
1906 |
|
1907 @property |
|
1908 def dependency_links(self): |
|
1909 return self.egg_info_lines('dependency_links.txt') |
|
1910 |
|
1911 _requirements_section_re = re.compile(r'\[(.*?)\]') |
|
1912 |
|
1913 def requirements(self, extras=()): |
|
1914 in_extra = None |
|
1915 for line in self.egg_info_lines('requires.txt'): |
|
1916 match = self._requirements_section_re.match(line) |
|
1917 if match: |
|
1918 in_extra = match.group(1) |
|
1919 continue |
|
1920 if in_extra and in_extra not in extras: |
|
1921 # Skip requirement for an extra we aren't requiring |
|
1922 continue |
|
1923 yield line |
|
1924 |
|
1925 @property |
|
1926 def absolute_versions(self): |
|
1927 for qualifier, version in self.req.specs: |
|
1928 if qualifier == '==': |
|
1929 yield version |
|
1930 |
|
1931 @property |
|
1932 def installed_version(self): |
|
1933 return self.pkg_info()['version'] |
|
1934 |
|
1935 def assert_source_matches_version(self): |
|
1936 assert self.source_dir |
|
1937 if self.comes_from is None: |
|
1938 # We don't check the versions of things explicitly installed. |
|
1939 # This makes, e.g., "pip Package==dev" possible |
|
1940 return |
|
1941 version = self.installed_version |
|
1942 if version not in self.req: |
|
1943 logger.fatal( |
|
1944 'Source in %s has the version %s, which does not match the requirement %s' |
|
1945 % (display_path(self.source_dir), version, self)) |
|
1946 raise InstallationError( |
|
1947 'Source in %s has version %s that conflicts with %s' |
|
1948 % (display_path(self.source_dir), version, self)) |
|
1949 else: |
|
1950 logger.debug('Source in %s has version %s, which satisfies requirement %s' |
|
1951 % (display_path(self.source_dir), version, self)) |
|
1952 |
|
1953 def update_editable(self, obtain=True): |
|
1954 if not self.url: |
|
1955 logger.info("Cannot update repository at %s; repository location is unknown" % self.source_dir) |
|
1956 return |
|
1957 assert self.editable |
|
1958 assert self.source_dir |
|
1959 if self.url.startswith('file:'): |
|
1960 # Static paths don't get updated |
|
1961 return |
|
1962 assert '+' in self.url, "bad url: %r" % self.url |
|
1963 if not self.update: |
|
1964 return |
|
1965 vc_type, url = self.url.split('+', 1) |
|
1966 backend = vcs.get_backend(vc_type) |
|
1967 if backend: |
|
1968 vcs_backend = backend(self.url) |
|
1969 if obtain: |
|
1970 vcs_backend.obtain(self.source_dir) |
|
1971 else: |
|
1972 vcs_backend.export(self.source_dir) |
|
1973 else: |
|
1974 assert 0, ( |
|
1975 'Unexpected version control type (in %s): %s' |
|
1976 % (self.url, vc_type)) |
|
1977 |
|
1978 def uninstall(self, auto_confirm=False): |
|
1979 """ |
|
1980 Uninstall the distribution currently satisfying this requirement. |
|
1981 |
|
1982 Prompts before removing or modifying files unless |
|
1983 ``auto_confirm`` is True. |
|
1984 |
|
1985 Refuses to delete or modify files outside of ``sys.prefix`` - |
|
1986 thus uninstallation within a virtual environment can only |
|
1987 modify that virtual environment, even if the virtualenv is |
|
1988 linked to global site-packages. |
|
1989 |
|
1990 """ |
|
1991 if not self.check_if_exists(): |
|
1992 raise UninstallationError("Cannot uninstall requirement %s, not installed" % (self.name,)) |
|
1993 dist = self.satisfied_by or self.conflicts_with |
|
1994 paths_to_remove = UninstallPathSet(dist, sys.prefix) |
|
1995 |
|
1996 pip_egg_info_path = os.path.join(dist.location, |
|
1997 dist.egg_name()) + '.egg-info' |
|
1998 easy_install_egg = dist.egg_name() + '.egg' |
|
1999 # This won't find a globally-installed develop egg if |
|
2000 # we're in a virtualenv. |
|
2001 # (There doesn't seem to be any metadata in the |
|
2002 # Distribution object for a develop egg that points back |
|
2003 # to its .egg-link and easy-install.pth files). That's |
|
2004 # OK, because we restrict ourselves to making changes |
|
2005 # within sys.prefix anyway. |
|
2006 develop_egg_link = os.path.join(site_packages, |
|
2007 dist.project_name) + '.egg-link' |
|
2008 if os.path.exists(pip_egg_info_path): |
|
2009 # package installed by pip |
|
2010 paths_to_remove.add(pip_egg_info_path) |
|
2011 if dist.has_metadata('installed-files.txt'): |
|
2012 for installed_file in dist.get_metadata('installed-files.txt').splitlines(): |
|
2013 path = os.path.normpath(os.path.join(pip_egg_info_path, installed_file)) |
|
2014 if os.path.exists(path): |
|
2015 paths_to_remove.add(path) |
|
2016 if dist.has_metadata('top_level.txt'): |
|
2017 for top_level_pkg in [p for p |
|
2018 in dist.get_metadata('top_level.txt').splitlines() |
|
2019 if p]: |
|
2020 path = os.path.join(dist.location, top_level_pkg) |
|
2021 if os.path.exists(path): |
|
2022 paths_to_remove.add(path) |
|
2023 elif os.path.exists(path + '.py'): |
|
2024 paths_to_remove.add(path + '.py') |
|
2025 if os.path.exists(path + '.pyc'): |
|
2026 paths_to_remove.add(path + '.pyc') |
|
2027 |
|
2028 elif dist.location.endswith(easy_install_egg): |
|
2029 # package installed by easy_install |
|
2030 paths_to_remove.add(dist.location) |
|
2031 easy_install_pth = os.path.join(os.path.dirname(dist.location), |
|
2032 'easy-install.pth') |
|
2033 paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg) |
|
2034 |
|
2035 elif os.path.isfile(develop_egg_link): |
|
2036 # develop egg |
|
2037 fh = open(develop_egg_link, 'r') |
|
2038 link_pointer = os.path.normcase(fh.readline().strip()) |
|
2039 fh.close() |
|
2040 assert (link_pointer == dist.location), 'Egg-link %s does not match installed location of %s (at %s)' % (link_pointer, self.name, dist.location) |
|
2041 paths_to_remove.add(develop_egg_link) |
|
2042 easy_install_pth = os.path.join(os.path.dirname(develop_egg_link), |
|
2043 'easy-install.pth') |
|
2044 paths_to_remove.add_pth(easy_install_pth, dist.location) |
|
2045 # fix location (so we can uninstall links to sources outside venv) |
|
2046 paths_to_remove.location = develop_egg_link |
|
2047 |
|
2048 # find distutils scripts= scripts |
|
2049 if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'): |
|
2050 for script in dist.metadata_listdir('scripts'): |
|
2051 paths_to_remove.add(os.path.join(bin_py, script)) |
|
2052 if sys.platform == 'win32': |
|
2053 paths_to_remove.add(os.path.join(bin_py, script) + '.bat') |
|
2054 |
|
2055 # find console_scripts |
|
2056 if dist.has_metadata('entry_points.txt'): |
|
2057 config = ConfigParser.SafeConfigParser() |
|
2058 config.readfp(FakeFile(dist.get_metadata_lines('entry_points.txt'))) |
|
2059 if config.has_section('console_scripts'): |
|
2060 for name, value in config.items('console_scripts'): |
|
2061 paths_to_remove.add(os.path.join(bin_py, name)) |
|
2062 if sys.platform == 'win32': |
|
2063 paths_to_remove.add(os.path.join(bin_py, name) + '.exe') |
|
2064 paths_to_remove.add(os.path.join(bin_py, name) + '-script.py') |
|
2065 |
|
2066 paths_to_remove.remove(auto_confirm) |
|
2067 self.uninstalled = paths_to_remove |
|
2068 |
|
2069 def rollback_uninstall(self): |
|
2070 if self.uninstalled: |
|
2071 self.uninstalled.rollback() |
|
2072 else: |
|
2073 logger.error("Can't rollback %s, nothing uninstalled." |
|
2074 % (self.project_name,)) |
|
2075 |
|
2076 def archive(self, build_dir): |
|
2077 assert self.source_dir |
|
2078 create_archive = True |
|
2079 archive_name = '%s-%s.zip' % (self.name, self.installed_version) |
|
2080 archive_path = os.path.join(build_dir, archive_name) |
|
2081 if os.path.exists(archive_path): |
|
2082 response = ask('The file %s exists. (i)gnore, (w)ipe, (b)ackup ' |
|
2083 % display_path(archive_path), ('i', 'w', 'b')) |
|
2084 if response == 'i': |
|
2085 create_archive = False |
|
2086 elif response == 'w': |
|
2087 logger.warn('Deleting %s' % display_path(archive_path)) |
|
2088 os.remove(archive_path) |
|
2089 elif response == 'b': |
|
2090 dest_file = backup_dir(archive_path) |
|
2091 logger.warn('Backing up %s to %s' |
|
2092 % (display_path(archive_path), display_path(dest_file))) |
|
2093 shutil.move(archive_path, dest_file) |
|
2094 if create_archive: |
|
2095 zip = zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) |
|
2096 dir = os.path.normcase(os.path.abspath(self.source_dir)) |
|
2097 for dirpath, dirnames, filenames in os.walk(dir): |
|
2098 if 'pip-egg-info' in dirnames: |
|
2099 dirnames.remove('pip-egg-info') |
|
2100 for dirname in dirnames: |
|
2101 dirname = os.path.join(dirpath, dirname) |
|
2102 name = self._clean_zip_name(dirname, dir) |
|
2103 zipdir = zipfile.ZipInfo(self.name + '/' + name + '/') |
|
2104 zipdir.external_attr = 0755 << 16L |
|
2105 zip.writestr(zipdir, '') |
|
2106 for filename in filenames: |
|
2107 if filename == 'pip-delete-this-directory.txt': |
|
2108 continue |
|
2109 filename = os.path.join(dirpath, filename) |
|
2110 name = self._clean_zip_name(filename, dir) |
|
2111 zip.write(filename, self.name + '/' + name) |
|
2112 zip.close() |
|
2113 logger.indent -= 2 |
|
2114 logger.notify('Saved %s' % display_path(archive_path)) |
|
2115 |
|
2116 def _clean_zip_name(self, name, prefix): |
|
2117 assert name.startswith(prefix+'/'), ( |
|
2118 "name %r doesn't start with prefix %r" % (name, prefix)) |
|
2119 name = name[len(prefix)+1:] |
|
2120 name = name.replace(os.path.sep, '/') |
|
2121 return name |
|
2122 |
|
2123 def install(self, install_options): |
|
2124 if self.editable: |
|
2125 self.install_editable() |
|
2126 return |
|
2127 temp_location = tempfile.mkdtemp('-record', 'pip-') |
|
2128 record_filename = os.path.join(temp_location, 'install-record.txt') |
|
2129 ## FIXME: I'm not sure if this is a reasonable location; probably not |
|
2130 ## but we can't put it in the default location, as that is a virtualenv symlink that isn't writable |
|
2131 header_dir = os.path.join(os.path.dirname(os.path.dirname(self.source_dir)), 'lib', 'include') |
|
2132 logger.notify('Running setup.py install for %s' % self.name) |
|
2133 logger.indent += 2 |
|
2134 try: |
|
2135 call_subprocess( |
|
2136 [sys.executable, '-c', |
|
2137 "import setuptools; __file__=%r; execfile(%r)" % (self.setup_py, self.setup_py), |
|
2138 'install', '--single-version-externally-managed', '--record', record_filename, |
|
2139 '--install-headers', header_dir] + install_options, |
|
2140 cwd=self.source_dir, filter_stdout=self._filter_install, show_stdout=False) |
|
2141 finally: |
|
2142 logger.indent -= 2 |
|
2143 self.install_succeeded = True |
|
2144 f = open(record_filename) |
|
2145 for line in f: |
|
2146 line = line.strip() |
|
2147 if line.endswith('.egg-info'): |
|
2148 egg_info_dir = line |
|
2149 break |
|
2150 else: |
|
2151 logger.warn('Could not find .egg-info directory in install record for %s' % self) |
|
2152 ## FIXME: put the record somewhere |
|
2153 return |
|
2154 f.close() |
|
2155 new_lines = [] |
|
2156 f = open(record_filename) |
|
2157 for line in f: |
|
2158 filename = line.strip() |
|
2159 if os.path.isdir(filename): |
|
2160 filename += os.path.sep |
|
2161 new_lines.append(make_path_relative(filename, egg_info_dir)) |
|
2162 f.close() |
|
2163 f = open(os.path.join(egg_info_dir, 'installed-files.txt'), 'w') |
|
2164 f.write('\n'.join(new_lines)+'\n') |
|
2165 f.close() |
|
2166 |
|
2167 def remove_temporary_source(self): |
|
2168 """Remove the source files from this requirement, if they are marked |
|
2169 for deletion""" |
|
2170 if self.is_bundle or os.path.exists(self.delete_marker_filename): |
|
2171 logger.info('Removing source in %s' % self.source_dir) |
|
2172 if self.source_dir: |
|
2173 shutil.rmtree(self.source_dir, ignore_errors=True, onerror=rmtree_errorhandler) |
|
2174 self.source_dir = None |
|
2175 if self._temp_build_dir and os.path.exists(self._temp_build_dir): |
|
2176 shutil.rmtree(self._temp_build_dir, ignore_errors=True, onerror=rmtree_errorhandler) |
|
2177 self._temp_build_dir = None |
|
2178 |
|
2179 def install_editable(self): |
|
2180 logger.notify('Running setup.py develop for %s' % self.name) |
|
2181 logger.indent += 2 |
|
2182 try: |
|
2183 ## FIXME: should we do --install-headers here too? |
|
2184 call_subprocess( |
|
2185 [sys.executable, '-c', |
|
2186 "import setuptools; __file__=%r; execfile(%r)" % (self.setup_py, self.setup_py), |
|
2187 'develop', '--no-deps'], cwd=self.source_dir, filter_stdout=self._filter_install, |
|
2188 show_stdout=False) |
|
2189 finally: |
|
2190 logger.indent -= 2 |
|
2191 self.install_succeeded = True |
|
2192 |
|
2193 def _filter_install(self, line): |
|
2194 level = Logger.NOTIFY |
|
2195 for regex in [r'^running .*', r'^writing .*', '^creating .*', '^[Cc]opying .*', |
|
2196 r'^reading .*', r"^removing .*\.egg-info' \(and everything under it\)$", |
|
2197 r'^byte-compiling ', |
|
2198 # Not sure what this warning is, but it seems harmless: |
|
2199 r"^warning: manifest_maker: standard file '-c' not found$"]: |
|
2200 if re.search(regex, line.strip()): |
|
2201 level = Logger.INFO |
|
2202 break |
|
2203 return (level, line) |
|
2204 |
|
2205 def check_if_exists(self): |
|
2206 """Find an installed distribution that satisfies or conflicts |
|
2207 with this requirement, and set self.satisfied_by or |
|
2208 self.conflicts_with appropriately.""" |
|
2209 if self.req is None: |
|
2210 return False |
|
2211 try: |
|
2212 self.satisfied_by = pkg_resources.get_distribution(self.req) |
|
2213 except pkg_resources.DistributionNotFound: |
|
2214 return False |
|
2215 except pkg_resources.VersionConflict: |
|
2216 self.conflicts_with = pkg_resources.get_distribution(self.req.project_name) |
|
2217 return True |
|
2218 |
|
2219 @property |
|
2220 def is_bundle(self): |
|
2221 if self._is_bundle is not None: |
|
2222 return self._is_bundle |
|
2223 base = self._temp_build_dir |
|
2224 if not base: |
|
2225 ## FIXME: this doesn't seem right: |
|
2226 return False |
|
2227 self._is_bundle = (os.path.exists(os.path.join(base, 'pip-manifest.txt')) |
|
2228 or os.path.exists(os.path.join(base, 'pyinstall-manifest.txt'))) |
|
2229 return self._is_bundle |
|
2230 |
|
2231 def bundle_requirements(self): |
|
2232 for dest_dir in self._bundle_editable_dirs: |
|
2233 package = os.path.basename(dest_dir) |
|
2234 ## FIXME: svnism: |
|
2235 for vcs_backend in vcs.backends: |
|
2236 url = rev = None |
|
2237 vcs_bundle_file = os.path.join( |
|
2238 dest_dir, vcs_backend.bundle_file) |
|
2239 if os.path.exists(vcs_bundle_file): |
|
2240 vc_type = vcs_backend.name |
|
2241 fp = open(vcs_bundle_file) |
|
2242 content = fp.read() |
|
2243 fp.close() |
|
2244 url, rev = vcs_backend().parse_vcs_bundle_file(content) |
|
2245 break |
|
2246 if url: |
|
2247 url = '%s+%s@%s' % (vc_type, url, rev) |
|
2248 else: |
|
2249 url = None |
|
2250 yield InstallRequirement( |
|
2251 package, self, editable=True, url=url, |
|
2252 update=False, source_dir=dest_dir) |
|
2253 for dest_dir in self._bundle_build_dirs: |
|
2254 package = os.path.basename(dest_dir) |
|
2255 yield InstallRequirement( |
|
2256 package, self, |
|
2257 source_dir=dest_dir) |
|
2258 |
|
2259 def move_bundle_files(self, dest_build_dir, dest_src_dir): |
|
2260 base = self._temp_build_dir |
|
2261 assert base |
|
2262 src_dir = os.path.join(base, 'src') |
|
2263 build_dir = os.path.join(base, 'build') |
|
2264 bundle_build_dirs = [] |
|
2265 bundle_editable_dirs = [] |
|
2266 for source_dir, dest_dir, dir_collection in [ |
|
2267 (src_dir, dest_src_dir, bundle_editable_dirs), |
|
2268 (build_dir, dest_build_dir, bundle_build_dirs)]: |
|
2269 if os.path.exists(source_dir): |
|
2270 for dirname in os.listdir(source_dir): |
|
2271 dest = os.path.join(dest_dir, dirname) |
|
2272 dir_collection.append(dest) |
|
2273 if os.path.exists(dest): |
|
2274 logger.warn('The directory %s (containing package %s) already exists; cannot move source from bundle %s' |
|
2275 % (dest, dirname, self)) |
|
2276 continue |
|
2277 if not os.path.exists(dest_dir): |
|
2278 logger.info('Creating directory %s' % dest_dir) |
|
2279 os.makedirs(dest_dir) |
|
2280 shutil.move(os.path.join(source_dir, dirname), dest) |
|
2281 if not os.listdir(source_dir): |
|
2282 os.rmdir(source_dir) |
|
2283 self._temp_build_dir = None |
|
2284 self._bundle_build_dirs = bundle_build_dirs |
|
2285 self._bundle_editable_dirs = bundle_editable_dirs |
|
2286 |
|
2287 @property |
|
2288 def delete_marker_filename(self): |
|
2289 assert self.source_dir |
|
2290 return os.path.join(self.source_dir, 'pip-delete-this-directory.txt') |
|
2291 |
|
2292 DELETE_MARKER_MESSAGE = '''\ |
|
2293 This file is placed here by pip to indicate the source was put |
|
2294 here by pip. |
|
2295 |
|
2296 Once this package is successfully installed this source code will be |
|
2297 deleted (unless you remove this file). |
|
2298 ''' |
|
2299 |
|
2300 class RequirementSet(object): |
|
2301 |
|
2302 def __init__(self, build_dir, src_dir, download_dir, download_cache=None, |
|
2303 upgrade=False, ignore_installed=False, |
|
2304 ignore_dependencies=False): |
|
2305 self.build_dir = build_dir |
|
2306 self.src_dir = src_dir |
|
2307 self.download_dir = download_dir |
|
2308 self.download_cache = download_cache |
|
2309 self.upgrade = upgrade |
|
2310 self.ignore_installed = ignore_installed |
|
2311 self.requirements = {} |
|
2312 # Mapping of alias: real_name |
|
2313 self.requirement_aliases = {} |
|
2314 self.unnamed_requirements = [] |
|
2315 self.ignore_dependencies = ignore_dependencies |
|
2316 self.successfully_downloaded = [] |
|
2317 self.successfully_installed = [] |
|
2318 |
|
2319 def __str__(self): |
|
2320 reqs = [req for req in self.requirements.values() |
|
2321 if not req.comes_from] |
|
2322 reqs.sort(key=lambda req: req.name.lower()) |
|
2323 return ' '.join([str(req.req) for req in reqs]) |
|
2324 |
|
2325 def add_requirement(self, install_req): |
|
2326 name = install_req.name |
|
2327 if not name: |
|
2328 self.unnamed_requirements.append(install_req) |
|
2329 else: |
|
2330 if self.has_requirement(name): |
|
2331 raise InstallationError( |
|
2332 'Double requirement given: %s (aready in %s, name=%r)' |
|
2333 % (install_req, self.get_requirement(name), name)) |
|
2334 self.requirements[name] = install_req |
|
2335 ## FIXME: what about other normalizations? E.g., _ vs. -? |
|
2336 if name.lower() != name: |
|
2337 self.requirement_aliases[name.lower()] = name |
|
2338 |
|
2339 def has_requirement(self, project_name): |
|
2340 for name in project_name, project_name.lower(): |
|
2341 if name in self.requirements or name in self.requirement_aliases: |
|
2342 return True |
|
2343 return False |
|
2344 |
|
2345 @property |
|
2346 def is_download(self): |
|
2347 if self.download_dir: |
|
2348 self.download_dir = os.path.expanduser(self.download_dir) |
|
2349 if os.path.exists(self.download_dir): |
|
2350 return True |
|
2351 else: |
|
2352 logger.fatal('Could not find download directory') |
|
2353 raise InstallationError( |
|
2354 "Could not find or access download directory '%s'" |
|
2355 % display_path(self.download_dir)) |
|
2356 return False |
|
2357 |
|
2358 def get_requirement(self, project_name): |
|
2359 for name in project_name, project_name.lower(): |
|
2360 if name in self.requirements: |
|
2361 return self.requirements[name] |
|
2362 if name in self.requirement_aliases: |
|
2363 return self.requirements[self.requirement_aliases[name]] |
|
2364 raise KeyError("No project with the name %r" % project_name) |
|
2365 |
|
2366 def uninstall(self, auto_confirm=False): |
|
2367 for req in self.requirements.values(): |
|
2368 req.uninstall(auto_confirm=auto_confirm) |
|
2369 |
|
2370 def install_files(self, finder, force_root_egg_info=False): |
|
2371 unnamed = list(self.unnamed_requirements) |
|
2372 reqs = self.requirements.values() |
|
2373 while reqs or unnamed: |
|
2374 if unnamed: |
|
2375 req_to_install = unnamed.pop(0) |
|
2376 else: |
|
2377 req_to_install = reqs.pop(0) |
|
2378 install = True |
|
2379 if not self.ignore_installed and not req_to_install.editable: |
|
2380 req_to_install.check_if_exists() |
|
2381 if req_to_install.satisfied_by: |
|
2382 if self.upgrade: |
|
2383 req_to_install.conflicts_with = req_to_install.satisfied_by |
|
2384 req_to_install.satisfied_by = None |
|
2385 else: |
|
2386 install = False |
|
2387 if req_to_install.satisfied_by: |
|
2388 logger.notify('Requirement already satisfied ' |
|
2389 '(use --upgrade to upgrade): %s' |
|
2390 % req_to_install) |
|
2391 if req_to_install.editable: |
|
2392 logger.notify('Obtaining %s' % req_to_install) |
|
2393 elif install: |
|
2394 if req_to_install.url and req_to_install.url.lower().startswith('file:'): |
|
2395 logger.notify('Unpacking %s' % display_path(url_to_filename(req_to_install.url))) |
|
2396 else: |
|
2397 logger.notify('Downloading/unpacking %s' % req_to_install) |
|
2398 logger.indent += 2 |
|
2399 is_bundle = False |
|
2400 try: |
|
2401 if req_to_install.editable: |
|
2402 if req_to_install.source_dir is None: |
|
2403 location = req_to_install.build_location(self.src_dir) |
|
2404 req_to_install.source_dir = location |
|
2405 else: |
|
2406 location = req_to_install.source_dir |
|
2407 if not os.path.exists(self.build_dir): |
|
2408 os.makedirs(self.build_dir) |
|
2409 req_to_install.update_editable(not self.is_download) |
|
2410 if self.is_download: |
|
2411 req_to_install.run_egg_info() |
|
2412 req_to_install.archive(self.download_dir) |
|
2413 else: |
|
2414 req_to_install.run_egg_info() |
|
2415 elif install: |
|
2416 location = req_to_install.build_location(self.build_dir, not self.is_download) |
|
2417 ## FIXME: is the existance of the checkout good enough to use it? I don't think so. |
|
2418 unpack = True |
|
2419 if not os.path.exists(os.path.join(location, 'setup.py')): |
|
2420 ## FIXME: this won't upgrade when there's an existing package unpacked in `location` |
|
2421 if req_to_install.url is None: |
|
2422 url = finder.find_requirement(req_to_install, upgrade=self.upgrade) |
|
2423 else: |
|
2424 ## FIXME: should req_to_install.url already be a link? |
|
2425 url = Link(req_to_install.url) |
|
2426 assert url |
|
2427 if url: |
|
2428 try: |
|
2429 self.unpack_url(url, location, self.is_download) |
|
2430 except urllib2.HTTPError, e: |
|
2431 logger.fatal('Could not install requirement %s because of error %s' |
|
2432 % (req_to_install, e)) |
|
2433 raise InstallationError( |
|
2434 'Could not install requirement %s because of HTTP error %s for URL %s' |
|
2435 % (req_to_install, e, url)) |
|
2436 else: |
|
2437 unpack = False |
|
2438 if unpack: |
|
2439 is_bundle = req_to_install.is_bundle |
|
2440 url = None |
|
2441 if is_bundle: |
|
2442 req_to_install.move_bundle_files(self.build_dir, self.src_dir) |
|
2443 for subreq in req_to_install.bundle_requirements(): |
|
2444 reqs.append(subreq) |
|
2445 self.add_requirement(subreq) |
|
2446 elif self.is_download: |
|
2447 req_to_install.source_dir = location |
|
2448 if url and url.scheme in vcs.all_schemes: |
|
2449 req_to_install.run_egg_info() |
|
2450 req_to_install.archive(self.download_dir) |
|
2451 else: |
|
2452 req_to_install.source_dir = location |
|
2453 req_to_install.run_egg_info() |
|
2454 if force_root_egg_info: |
|
2455 # We need to run this to make sure that the .egg-info/ |
|
2456 # directory is created for packing in the bundle |
|
2457 req_to_install.run_egg_info(force_root_egg_info=True) |
|
2458 req_to_install.assert_source_matches_version() |
|
2459 f = open(req_to_install.delete_marker_filename, 'w') |
|
2460 f.write(DELETE_MARKER_MESSAGE) |
|
2461 f.close() |
|
2462 if not is_bundle and not self.is_download: |
|
2463 ## FIXME: shouldn't be globally added: |
|
2464 finder.add_dependency_links(req_to_install.dependency_links) |
|
2465 ## FIXME: add extras in here: |
|
2466 if not self.ignore_dependencies: |
|
2467 for req in req_to_install.requirements(): |
|
2468 try: |
|
2469 name = pkg_resources.Requirement.parse(req).project_name |
|
2470 except ValueError, e: |
|
2471 ## FIXME: proper warning |
|
2472 logger.error('Invalid requirement: %r (%s) in requirement %s' % (req, e, req_to_install)) |
|
2473 continue |
|
2474 if self.has_requirement(name): |
|
2475 ## FIXME: check for conflict |
|
2476 continue |
|
2477 subreq = InstallRequirement(req, req_to_install) |
|
2478 reqs.append(subreq) |
|
2479 self.add_requirement(subreq) |
|
2480 if req_to_install.name not in self.requirements: |
|
2481 self.requirements[req_to_install.name] = req_to_install |
|
2482 else: |
|
2483 req_to_install.remove_temporary_source() |
|
2484 if install: |
|
2485 self.successfully_downloaded.append(req_to_install) |
|
2486 finally: |
|
2487 logger.indent -= 2 |
|
2488 |
|
2489 def unpack_url(self, link, location, only_download=False): |
|
2490 if only_download: |
|
2491 location = self.download_dir |
|
2492 for backend in vcs.backends: |
|
2493 if link.scheme in backend.schemes: |
|
2494 vcs_backend = backend(link.url) |
|
2495 if only_download: |
|
2496 vcs_backend.export(location) |
|
2497 else: |
|
2498 vcs_backend.unpack(location) |
|
2499 return |
|
2500 dir = tempfile.mkdtemp() |
|
2501 if link.url.lower().startswith('file:'): |
|
2502 source = url_to_filename(link.url) |
|
2503 content_type = mimetypes.guess_type(source)[0] |
|
2504 self.unpack_file(source, location, content_type, link) |
|
2505 return |
|
2506 md5_hash = link.md5_hash |
|
2507 target_url = link.url.split('#', 1)[0] |
|
2508 target_file = None |
|
2509 if self.download_cache: |
|
2510 if not os.path.isdir(self.download_cache): |
|
2511 logger.indent -= 2 |
|
2512 logger.notify('Creating supposed download cache at %s' % self.download_cache) |
|
2513 logger.indent += 2 |
|
2514 os.makedirs(self.download_cache) |
|
2515 target_file = os.path.join(self.download_cache, |
|
2516 urllib.quote(target_url, '')) |
|
2517 if (target_file and os.path.exists(target_file) |
|
2518 and os.path.exists(target_file+'.content-type')): |
|
2519 fp = open(target_file+'.content-type') |
|
2520 content_type = fp.read().strip() |
|
2521 fp.close() |
|
2522 if md5_hash: |
|
2523 download_hash = md5() |
|
2524 fp = open(target_file, 'rb') |
|
2525 while 1: |
|
2526 chunk = fp.read(4096) |
|
2527 if not chunk: |
|
2528 break |
|
2529 download_hash.update(chunk) |
|
2530 fp.close() |
|
2531 temp_location = target_file |
|
2532 logger.notify('Using download cache from %s' % target_file) |
|
2533 else: |
|
2534 try: |
|
2535 resp = urllib2.urlopen(target_url) |
|
2536 except urllib2.HTTPError, e: |
|
2537 logger.fatal("HTTP error %s while getting %s" % (e.code, link)) |
|
2538 raise |
|
2539 except IOError, e: |
|
2540 # Typically an FTP error |
|
2541 logger.fatal("Error %s while getting %s" % (e, link)) |
|
2542 raise |
|
2543 content_type = resp.info()['content-type'] |
|
2544 filename = link.filename |
|
2545 ext = splitext(filename)[1] |
|
2546 if not ext: |
|
2547 ext = mimetypes.guess_extension(content_type) |
|
2548 if ext: |
|
2549 filename += ext |
|
2550 if not ext and link.url != resp.geturl(): |
|
2551 ext = os.path.splitext(resp.geturl())[1] |
|
2552 if ext: |
|
2553 filename += ext |
|
2554 temp_location = os.path.join(dir, filename) |
|
2555 fp = open(temp_location, 'wb') |
|
2556 if md5_hash: |
|
2557 download_hash = md5() |
|
2558 try: |
|
2559 total_length = int(resp.info()['content-length']) |
|
2560 except (ValueError, KeyError): |
|
2561 total_length = 0 |
|
2562 downloaded = 0 |
|
2563 show_progress = total_length > 40*1000 or not total_length |
|
2564 show_url = link.show_url |
|
2565 try: |
|
2566 if show_progress: |
|
2567 ## FIXME: the URL can get really long in this message: |
|
2568 if total_length: |
|
2569 logger.start_progress('Downloading %s (%s): ' % (show_url, format_size(total_length))) |
|
2570 else: |
|
2571 logger.start_progress('Downloading %s (unknown size): ' % show_url) |
|
2572 else: |
|
2573 logger.notify('Downloading %s' % show_url) |
|
2574 logger.debug('Downloading from URL %s' % link) |
|
2575 while 1: |
|
2576 chunk = resp.read(4096) |
|
2577 if not chunk: |
|
2578 break |
|
2579 downloaded += len(chunk) |
|
2580 if show_progress: |
|
2581 if not total_length: |
|
2582 logger.show_progress('%s' % format_size(downloaded)) |
|
2583 else: |
|
2584 logger.show_progress('%3i%% %s' % (100*downloaded/total_length, format_size(downloaded))) |
|
2585 if md5_hash: |
|
2586 download_hash.update(chunk) |
|
2587 fp.write(chunk) |
|
2588 fp.close() |
|
2589 finally: |
|
2590 if show_progress: |
|
2591 logger.end_progress('%s downloaded' % format_size(downloaded)) |
|
2592 if md5_hash: |
|
2593 download_hash = download_hash.hexdigest() |
|
2594 if download_hash != md5_hash: |
|
2595 logger.fatal("MD5 hash of the package %s (%s) doesn't match the expected hash %s!" |
|
2596 % (link, download_hash, md5_hash)) |
|
2597 raise InstallationError('Bad MD5 hash for package %s' % link) |
|
2598 if only_download: |
|
2599 self.copy_file(temp_location, location, content_type, link) |
|
2600 else: |
|
2601 self.unpack_file(temp_location, location, content_type, link) |
|
2602 if target_file and target_file != temp_location: |
|
2603 logger.notify('Storing download in cache at %s' % display_path(target_file)) |
|
2604 shutil.copyfile(temp_location, target_file) |
|
2605 fp = open(target_file+'.content-type', 'w') |
|
2606 fp.write(content_type) |
|
2607 fp.close() |
|
2608 os.unlink(temp_location) |
|
2609 if target_file is None: |
|
2610 os.unlink(temp_location) |
|
2611 |
|
2612 def copy_file(self, filename, location, content_type, link): |
|
2613 copy = True |
|
2614 download_location = os.path.join(location, link.filename) |
|
2615 if os.path.exists(download_location): |
|
2616 response = ask('The file %s exists. (i)gnore, (w)ipe, (b)ackup ' |
|
2617 % display_path(download_location), ('i', 'w', 'b')) |
|
2618 if response == 'i': |
|
2619 copy = False |
|
2620 elif response == 'w': |
|
2621 logger.warn('Deleting %s' % display_path(download_location)) |
|
2622 os.remove(download_location) |
|
2623 elif response == 'b': |
|
2624 dest_file = backup_dir(download_location) |
|
2625 logger.warn('Backing up %s to %s' |
|
2626 % (display_path(download_location), display_path(dest_file))) |
|
2627 shutil.move(download_location, dest_file) |
|
2628 if copy: |
|
2629 shutil.copy(filename, download_location) |
|
2630 logger.indent -= 2 |
|
2631 logger.notify('Saved %s' % display_path(download_location)) |
|
2632 |
|
2633 def unpack_file(self, filename, location, content_type, link): |
|
2634 if (content_type == 'application/zip' |
|
2635 or filename.endswith('.zip') |
|
2636 or filename.endswith('.pybundle') |
|
2637 or zipfile.is_zipfile(filename)): |
|
2638 self.unzip_file(filename, location, flatten=not filename.endswith('.pybundle')) |
|
2639 elif (content_type == 'application/x-gzip' |
|
2640 or tarfile.is_tarfile(filename) |
|
2641 or splitext(filename)[1].lower() in ('.tar', '.tar.gz', '.tar.bz2', '.tgz', '.tbz')): |
|
2642 self.untar_file(filename, location) |
|
2643 elif (content_type and content_type.startswith('text/html') |
|
2644 and is_svn_page(file_contents(filename))): |
|
2645 # We don't really care about this |
|
2646 Subversion('svn+' + link.url).unpack(location) |
|
2647 else: |
|
2648 ## FIXME: handle? |
|
2649 ## FIXME: magic signatures? |
|
2650 logger.fatal('Cannot unpack file %s (downloaded from %s, content-type: %s); cannot detect archive format' |
|
2651 % (filename, location, content_type)) |
|
2652 raise InstallationError('Cannot determine archive format of %s' % location) |
|
2653 |
|
2654 def unzip_file(self, filename, location, flatten=True): |
|
2655 """Unzip the file (zip file located at filename) to the destination |
|
2656 location""" |
|
2657 if not os.path.exists(location): |
|
2658 os.makedirs(location) |
|
2659 zipfp = open(filename, 'rb') |
|
2660 try: |
|
2661 zip = zipfile.ZipFile(zipfp) |
|
2662 leading = has_leading_dir(zip.namelist()) and flatten |
|
2663 for name in zip.namelist(): |
|
2664 data = zip.read(name) |
|
2665 fn = name |
|
2666 if leading: |
|
2667 fn = split_leading_dir(name)[1] |
|
2668 fn = os.path.join(location, fn) |
|
2669 dir = os.path.dirname(fn) |
|
2670 if not os.path.exists(dir): |
|
2671 os.makedirs(dir) |
|
2672 if fn.endswith('/') or fn.endswith('\\'): |
|
2673 # A directory |
|
2674 if not os.path.exists(fn): |
|
2675 os.makedirs(fn) |
|
2676 else: |
|
2677 fp = open(fn, 'wb') |
|
2678 try: |
|
2679 fp.write(data) |
|
2680 finally: |
|
2681 fp.close() |
|
2682 finally: |
|
2683 zipfp.close() |
|
2684 |
|
2685 def untar_file(self, filename, location): |
|
2686 """Untar the file (tar file located at filename) to the destination location""" |
|
2687 if not os.path.exists(location): |
|
2688 os.makedirs(location) |
|
2689 if filename.lower().endswith('.gz') or filename.lower().endswith('.tgz'): |
|
2690 mode = 'r:gz' |
|
2691 elif filename.lower().endswith('.bz2') or filename.lower().endswith('.tbz'): |
|
2692 mode = 'r:bz2' |
|
2693 elif filename.lower().endswith('.tar'): |
|
2694 mode = 'r' |
|
2695 else: |
|
2696 logger.warn('Cannot determine compression type for file %s' % filename) |
|
2697 mode = 'r:*' |
|
2698 tar = tarfile.open(filename, mode) |
|
2699 try: |
|
2700 leading = has_leading_dir([member.name for member in tar.getmembers()]) |
|
2701 for member in tar.getmembers(): |
|
2702 fn = member.name |
|
2703 if leading: |
|
2704 fn = split_leading_dir(fn)[1] |
|
2705 path = os.path.join(location, fn) |
|
2706 if member.isdir(): |
|
2707 if not os.path.exists(path): |
|
2708 os.makedirs(path) |
|
2709 else: |
|
2710 try: |
|
2711 fp = tar.extractfile(member) |
|
2712 except (KeyError, AttributeError), e: |
|
2713 # Some corrupt tar files seem to produce this |
|
2714 # (specifically bad symlinks) |
|
2715 logger.warn( |
|
2716 'In the tar file %s the member %s is invalid: %s' |
|
2717 % (filename, member.name, e)) |
|
2718 continue |
|
2719 if not os.path.exists(os.path.dirname(path)): |
|
2720 os.makedirs(os.path.dirname(path)) |
|
2721 destfp = open(path, 'wb') |
|
2722 try: |
|
2723 shutil.copyfileobj(fp, destfp) |
|
2724 finally: |
|
2725 destfp.close() |
|
2726 fp.close() |
|
2727 finally: |
|
2728 tar.close() |
|
2729 |
|
2730 def install(self, install_options): |
|
2731 """Install everything in this set (after having downloaded and unpacked the packages)""" |
|
2732 to_install = sorted([r for r in self.requirements.values() |
|
2733 if self.upgrade or not r.satisfied_by], |
|
2734 key=lambda p: p.name.lower()) |
|
2735 if to_install: |
|
2736 logger.notify('Installing collected packages: %s' % (', '.join([req.name for req in to_install]))) |
|
2737 logger.indent += 2 |
|
2738 try: |
|
2739 for requirement in to_install: |
|
2740 if requirement.conflicts_with: |
|
2741 logger.notify('Found existing installation: %s' |
|
2742 % requirement.conflicts_with) |
|
2743 logger.indent += 2 |
|
2744 try: |
|
2745 requirement.uninstall(auto_confirm=True) |
|
2746 finally: |
|
2747 logger.indent -= 2 |
|
2748 try: |
|
2749 requirement.install(install_options) |
|
2750 except: |
|
2751 # if install did not succeed, rollback previous uninstall |
|
2752 if requirement.conflicts_with and not requirement.install_succeeded: |
|
2753 requirement.rollback_uninstall() |
|
2754 raise |
|
2755 requirement.remove_temporary_source() |
|
2756 finally: |
|
2757 logger.indent -= 2 |
|
2758 self.successfully_installed = to_install |
|
2759 |
|
2760 def create_bundle(self, bundle_filename): |
|
2761 ## FIXME: can't decide which is better; zip is easier to read |
|
2762 ## random files from, but tar.bz2 is smaller and not as lame a |
|
2763 ## format. |
|
2764 |
|
2765 ## FIXME: this file should really include a manifest of the |
|
2766 ## packages, maybe some other metadata files. It would make |
|
2767 ## it easier to detect as well. |
|
2768 zip = zipfile.ZipFile(bundle_filename, 'w', zipfile.ZIP_DEFLATED) |
|
2769 vcs_dirs = [] |
|
2770 for dir, basename in (self.build_dir, 'build'), (self.src_dir, 'src'): |
|
2771 dir = os.path.normcase(os.path.abspath(dir)) |
|
2772 for dirpath, dirnames, filenames in os.walk(dir): |
|
2773 for backend in vcs.backends: |
|
2774 vcs_backend = backend() |
|
2775 vcs_url = vcs_rev = None |
|
2776 if vcs_backend.dirname in dirnames: |
|
2777 for vcs_dir in vcs_dirs: |
|
2778 if dirpath.startswith(vcs_dir): |
|
2779 # vcs bundle file already in parent directory |
|
2780 break |
|
2781 else: |
|
2782 vcs_url, vcs_rev = vcs_backend.get_info( |
|
2783 os.path.join(dir, dirpath)) |
|
2784 vcs_dirs.append(dirpath) |
|
2785 vcs_bundle_file = vcs_backend.bundle_file |
|
2786 vcs_guide = vcs_backend.guide % {'url': vcs_url, |
|
2787 'rev': vcs_rev} |
|
2788 dirnames.remove(vcs_backend.dirname) |
|
2789 break |
|
2790 if 'pip-egg-info' in dirnames: |
|
2791 dirnames.remove('pip-egg-info') |
|
2792 for dirname in dirnames: |
|
2793 dirname = os.path.join(dirpath, dirname) |
|
2794 name = self._clean_zip_name(dirname, dir) |
|
2795 zip.writestr(basename + '/' + name + '/', '') |
|
2796 for filename in filenames: |
|
2797 if filename == 'pip-delete-this-directory.txt': |
|
2798 continue |
|
2799 filename = os.path.join(dirpath, filename) |
|
2800 name = self._clean_zip_name(filename, dir) |
|
2801 zip.write(filename, basename + '/' + name) |
|
2802 if vcs_url: |
|
2803 name = os.path.join(dirpath, vcs_bundle_file) |
|
2804 name = self._clean_zip_name(name, dir) |
|
2805 zip.writestr(basename + '/' + name, vcs_guide) |
|
2806 |
|
2807 zip.writestr('pip-manifest.txt', self.bundle_requirements()) |
|
2808 zip.close() |
|
2809 # Unlike installation, this will always delete the build directories |
|
2810 logger.info('Removing temporary build dir %s and source dir %s' |
|
2811 % (self.build_dir, self.src_dir)) |
|
2812 for dir in self.build_dir, self.src_dir: |
|
2813 if os.path.exists(dir): |
|
2814 shutil.rmtree(dir) |
|
2815 |
|
2816 |
|
2817 BUNDLE_HEADER = '''\ |
|
2818 # This is a pip bundle file, that contains many source packages |
|
2819 # that can be installed as a group. You can install this like: |
|
2820 # pip this_file.zip |
|
2821 # The rest of the file contains a list of all the packages included: |
|
2822 ''' |
|
2823 |
|
2824 def bundle_requirements(self): |
|
2825 parts = [self.BUNDLE_HEADER] |
|
2826 for req in sorted( |
|
2827 [req for req in self.requirements.values() |
|
2828 if not req.comes_from], |
|
2829 key=lambda x: x.name): |
|
2830 parts.append('%s==%s\n' % (req.name, req.installed_version)) |
|
2831 parts.append('# These packages were installed to satisfy the above requirements:\n') |
|
2832 for req in sorted( |
|
2833 [req for req in self.requirements.values() |
|
2834 if req.comes_from], |
|
2835 key=lambda x: x.name): |
|
2836 parts.append('%s==%s\n' % (req.name, req.installed_version)) |
|
2837 ## FIXME: should we do something with self.unnamed_requirements? |
|
2838 return ''.join(parts) |
|
2839 |
|
2840 def _clean_zip_name(self, name, prefix): |
|
2841 assert name.startswith(prefix+'/'), ( |
|
2842 "name %r doesn't start with prefix %r" % (name, prefix)) |
|
2843 name = name[len(prefix)+1:] |
|
2844 name = name.replace(os.path.sep, '/') |
|
2845 return name |
|
2846 |
|
2847 class HTMLPage(object): |
|
2848 """Represents one page, along with its URL""" |
|
2849 |
|
2850 ## FIXME: these regexes are horrible hacks: |
|
2851 _homepage_re = re.compile(r'<th>\s*home\s*page', re.I) |
|
2852 _download_re = re.compile(r'<th>\s*download\s+url', re.I) |
|
2853 ## These aren't so aweful: |
|
2854 _rel_re = re.compile("""<[^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*>""", re.I) |
|
2855 _href_re = re.compile('href=(?:"([^"]*)"|\'([^\']*)\'|([^>\\s\\n]*))', re.I|re.S) |
|
2856 _base_re = re.compile(r"""<base\s+href\s*=\s*['"]?([^'">]+)""", re.I) |
|
2857 |
|
2858 def __init__(self, content, url, headers=None): |
|
2859 self.content = content |
|
2860 self.url = url |
|
2861 self.headers = headers |
|
2862 |
|
2863 def __str__(self): |
|
2864 return self.url |
|
2865 |
|
2866 @classmethod |
|
2867 def get_page(cls, link, req, cache=None, skip_archives=True): |
|
2868 url = link.url |
|
2869 url = url.split('#', 1)[0] |
|
2870 if cache.too_many_failures(url): |
|
2871 return None |
|
2872 if url.lower().startswith('svn'): |
|
2873 logger.debug('Cannot look at svn URL %s' % link) |
|
2874 return None |
|
2875 if cache is not None: |
|
2876 inst = cache.get_page(url) |
|
2877 if inst is not None: |
|
2878 return inst |
|
2879 try: |
|
2880 if skip_archives: |
|
2881 if cache is not None: |
|
2882 if cache.is_archive(url): |
|
2883 return None |
|
2884 filename = link.filename |
|
2885 for bad_ext in ['.tar', '.tar.gz', '.tar.bz2', '.tgz', '.zip']: |
|
2886 if filename.endswith(bad_ext): |
|
2887 content_type = cls._get_content_type(url) |
|
2888 if content_type.lower().startswith('text/html'): |
|
2889 break |
|
2890 else: |
|
2891 logger.debug('Skipping page %s because of Content-Type: %s' % (link, content_type)) |
|
2892 if cache is not None: |
|
2893 cache.set_is_archive(url) |
|
2894 return None |
|
2895 logger.debug('Getting page %s' % url) |
|
2896 resp = urllib2.urlopen(url) |
|
2897 real_url = resp.geturl() |
|
2898 headers = resp.info() |
|
2899 inst = cls(resp.read(), real_url, headers) |
|
2900 except (urllib2.HTTPError, urllib2.URLError, socket.timeout, socket.error), e: |
|
2901 desc = str(e) |
|
2902 if isinstance(e, socket.timeout): |
|
2903 log_meth = logger.info |
|
2904 level =1 |
|
2905 desc = 'timed out' |
|
2906 elif isinstance(e, urllib2.URLError): |
|
2907 log_meth = logger.info |
|
2908 if hasattr(e, 'reason') and isinstance(e.reason, socket.timeout): |
|
2909 desc = 'timed out' |
|
2910 level = 1 |
|
2911 else: |
|
2912 level = 2 |
|
2913 elif isinstance(e, urllib2.HTTPError) and e.code == 404: |
|
2914 ## FIXME: notify? |
|
2915 log_meth = logger.info |
|
2916 level = 2 |
|
2917 else: |
|
2918 log_meth = logger.info |
|
2919 level = 1 |
|
2920 log_meth('Could not fetch URL %s: %s' % (link, desc)) |
|
2921 log_meth('Will skip URL %s when looking for download links for %s' % (link.url, req)) |
|
2922 if cache is not None: |
|
2923 cache.add_page_failure(url, level) |
|
2924 return None |
|
2925 if cache is not None: |
|
2926 cache.add_page([url, real_url], inst) |
|
2927 return inst |
|
2928 |
|
2929 @staticmethod |
|
2930 def _get_content_type(url): |
|
2931 """Get the Content-Type of the given url, using a HEAD request""" |
|
2932 scheme, netloc, path, query, fragment = urlparse.urlsplit(url) |
|
2933 if scheme == 'http': |
|
2934 ConnClass = httplib.HTTPConnection |
|
2935 elif scheme == 'https': |
|
2936 ConnClass = httplib.HTTPSConnection |
|
2937 else: |
|
2938 ## FIXME: some warning or something? |
|
2939 ## assertion error? |
|
2940 return '' |
|
2941 if query: |
|
2942 path += '?' + query |
|
2943 conn = ConnClass(netloc) |
|
2944 try: |
|
2945 conn.request('HEAD', path, headers={'Host': netloc}) |
|
2946 resp = conn.getresponse() |
|
2947 if resp.status != 200: |
|
2948 ## FIXME: doesn't handle redirects |
|
2949 return '' |
|
2950 return resp.getheader('Content-Type') or '' |
|
2951 finally: |
|
2952 conn.close() |
|
2953 |
|
2954 @property |
|
2955 def base_url(self): |
|
2956 if not hasattr(self, "_base_url"): |
|
2957 match = self._base_re.search(self.content) |
|
2958 if match: |
|
2959 self._base_url = match.group(1) |
|
2960 else: |
|
2961 self._base_url = self.url |
|
2962 return self._base_url |
|
2963 |
|
2964 @property |
|
2965 def links(self): |
|
2966 """Yields all links in the page""" |
|
2967 for match in self._href_re.finditer(self.content): |
|
2968 url = match.group(1) or match.group(2) or match.group(3) |
|
2969 url = self.clean_link(urlparse.urljoin(self.base_url, url)) |
|
2970 yield Link(url, self) |
|
2971 |
|
2972 def rel_links(self): |
|
2973 for url in self.explicit_rel_links(): |
|
2974 yield url |
|
2975 for url in self.scraped_rel_links(): |
|
2976 yield url |
|
2977 |
|
2978 def explicit_rel_links(self, rels=('homepage', 'download')): |
|
2979 """Yields all links with the given relations""" |
|
2980 for match in self._rel_re.finditer(self.content): |
|
2981 found_rels = match.group(1).lower().split() |
|
2982 for rel in rels: |
|
2983 if rel in found_rels: |
|
2984 break |
|
2985 else: |
|
2986 continue |
|
2987 match = self._href_re.search(match.group(0)) |
|
2988 if not match: |
|
2989 continue |
|
2990 url = match.group(1) or match.group(2) or match.group(3) |
|
2991 url = self.clean_link(urlparse.urljoin(self.base_url, url)) |
|
2992 yield Link(url, self) |
|
2993 |
|
2994 def scraped_rel_links(self): |
|
2995 for regex in (self._homepage_re, self._download_re): |
|
2996 match = regex.search(self.content) |
|
2997 if not match: |
|
2998 continue |
|
2999 href_match = self._href_re.search(self.content, pos=match.end()) |
|
3000 if not href_match: |
|
3001 continue |
|
3002 url = match.group(1) or match.group(2) or match.group(3) |
|
3003 if not url: |
|
3004 continue |
|
3005 url = self.clean_link(urlparse.urljoin(self.base_url, url)) |
|
3006 yield Link(url, self) |
|
3007 |
|
3008 _clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I) |
|
3009 |
|
3010 def clean_link(self, url): |
|
3011 """Makes sure a link is fully encoded. That is, if a ' ' shows up in |
|
3012 the link, it will be rewritten to %20 (while not over-quoting |
|
3013 % or other characters).""" |
|
3014 return self._clean_re.sub( |
|
3015 lambda match: '%%%2x' % ord(match.group(0)), url) |
|
3016 |
|
3017 class PageCache(object): |
|
3018 """Cache of HTML pages""" |
|
3019 |
|
3020 failure_limit = 3 |
|
3021 |
|
3022 def __init__(self): |
|
3023 self._failures = {} |
|
3024 self._pages = {} |
|
3025 self._archives = {} |
|
3026 |
|
3027 def too_many_failures(self, url): |
|
3028 return self._failures.get(url, 0) >= self.failure_limit |
|
3029 |
|
3030 def get_page(self, url): |
|
3031 return self._pages.get(url) |
|
3032 |
|
3033 def is_archive(self, url): |
|
3034 return self._archives.get(url, False) |
|
3035 |
|
3036 def set_is_archive(self, url, value=True): |
|
3037 self._archives[url] = value |
|
3038 |
|
3039 def add_page_failure(self, url, level): |
|
3040 self._failures[url] = self._failures.get(url, 0)+level |
|
3041 |
|
3042 def add_page(self, urls, page): |
|
3043 for url in urls: |
|
3044 self._pages[url] = page |
|
3045 |
|
3046 class Link(object): |
|
3047 |
|
3048 def __init__(self, url, comes_from=None): |
|
3049 self.url = url |
|
3050 self.comes_from = comes_from |
|
3051 |
|
3052 def __str__(self): |
|
3053 if self.comes_from: |
|
3054 return '%s (from %s)' % (self.url, self.comes_from) |
|
3055 else: |
|
3056 return self.url |
|
3057 |
|
3058 def __repr__(self): |
|
3059 return '<Link %s>' % self |
|
3060 |
|
3061 def __eq__(self, other): |
|
3062 return self.url == other.url |
|
3063 |
|
3064 def __hash__(self): |
|
3065 return hash(self.url) |
|
3066 |
|
3067 @property |
|
3068 def filename(self): |
|
3069 url = self.url |
|
3070 url = url.split('#', 1)[0] |
|
3071 url = url.split('?', 1)[0] |
|
3072 url = url.rstrip('/') |
|
3073 name = posixpath.basename(url) |
|
3074 assert name, ( |
|
3075 'URL %r produced no filename' % url) |
|
3076 return name |
|
3077 |
|
3078 @property |
|
3079 def scheme(self): |
|
3080 return urlparse.urlsplit(self.url)[0] |
|
3081 |
|
3082 @property |
|
3083 def path(self): |
|
3084 return urlparse.urlsplit(self.url)[2] |
|
3085 |
|
3086 def splitext(self): |
|
3087 return splitext(posixpath.basename(self.path.rstrip('/'))) |
|
3088 |
|
3089 _egg_fragment_re = re.compile(r'#egg=([^&]*)') |
|
3090 |
|
3091 @property |
|
3092 def egg_fragment(self): |
|
3093 match = self._egg_fragment_re.search(self.url) |
|
3094 if not match: |
|
3095 return None |
|
3096 return match.group(1) |
|
3097 |
|
3098 _md5_re = re.compile(r'md5=([a-f0-9]+)') |
|
3099 |
|
3100 @property |
|
3101 def md5_hash(self): |
|
3102 match = self._md5_re.search(self.url) |
|
3103 if match: |
|
3104 return match.group(1) |
|
3105 return None |
|
3106 |
|
3107 @property |
|
3108 def show_url(self): |
|
3109 return posixpath.basename(self.url.split('#', 1)[0].split('?', 1)[0]) |
|
3110 |
|
3111 ############################################################ |
|
3112 ## Writing freeze files |
|
3113 |
|
3114 |
|
3115 class FrozenRequirement(object): |
|
3116 |
|
3117 def __init__(self, name, req, editable, comments=()): |
|
3118 self.name = name |
|
3119 self.req = req |
|
3120 self.editable = editable |
|
3121 self.comments = comments |
|
3122 |
|
3123 _rev_re = re.compile(r'-r(\d+)$') |
|
3124 _date_re = re.compile(r'-(20\d\d\d\d\d\d)$') |
|
3125 |
|
3126 @classmethod |
|
3127 def from_dist(cls, dist, dependency_links, find_tags=False): |
|
3128 location = os.path.normcase(os.path.abspath(dist.location)) |
|
3129 comments = [] |
|
3130 if vcs.get_backend_name(location): |
|
3131 editable = True |
|
3132 req = get_src_requirement(dist, location, find_tags) |
|
3133 if req is None: |
|
3134 logger.warn('Could not determine repository location of %s' % location) |
|
3135 comments.append('## !! Could not determine repository location') |
|
3136 req = dist.as_requirement() |
|
3137 editable = False |
|
3138 else: |
|
3139 editable = False |
|
3140 req = dist.as_requirement() |
|
3141 specs = req.specs |
|
3142 assert len(specs) == 1 and specs[0][0] == '==' |
|
3143 version = specs[0][1] |
|
3144 ver_match = cls._rev_re.search(version) |
|
3145 date_match = cls._date_re.search(version) |
|
3146 if ver_match or date_match: |
|
3147 svn_backend = vcs.get_backend('svn') |
|
3148 if svn_backend: |
|
3149 svn_location = svn_backend( |
|
3150 ).get_location(dist, dependency_links) |
|
3151 if not svn_location: |
|
3152 logger.warn( |
|
3153 'Warning: cannot find svn location for %s' % req) |
|
3154 comments.append('## FIXME: could not find svn URL in dependency_links for this package:') |
|
3155 else: |
|
3156 comments.append('# Installing as editable to satisfy requirement %s:' % req) |
|
3157 if ver_match: |
|
3158 rev = ver_match.group(1) |
|
3159 else: |
|
3160 rev = '{%s}' % date_match.group(1) |
|
3161 editable = True |
|
3162 req = 'svn+%s@%s#egg=%s' % (svn_location, rev, cls.egg_name(dist)) |
|
3163 return cls(dist.project_name, req, editable, comments) |
|
3164 |
|
3165 @staticmethod |
|
3166 def egg_name(dist): |
|
3167 name = dist.egg_name() |
|
3168 match = re.search(r'-py\d\.\d$', name) |
|
3169 if match: |
|
3170 name = name[:match.start()] |
|
3171 return name |
|
3172 |
|
3173 def __str__(self): |
|
3174 req = self.req |
|
3175 if self.editable: |
|
3176 req = '-e %s' % req |
|
3177 return '\n'.join(list(self.comments)+[str(req)])+'\n' |
|
3178 |
|
3179 class VersionControl(object): |
|
3180 name = '' |
|
3181 dirname = '' |
|
3182 |
|
3183 def __init__(self, url=None, *args, **kwargs): |
|
3184 self.url = url |
|
3185 self._cmd = None |
|
3186 super(VersionControl, self).__init__(*args, **kwargs) |
|
3187 |
|
3188 def _filter(self, line): |
|
3189 return (Logger.INFO, line) |
|
3190 |
|
3191 @property |
|
3192 def cmd(self): |
|
3193 if self._cmd is not None: |
|
3194 return self._cmd |
|
3195 command = find_command(self.name) |
|
3196 if command is None: |
|
3197 raise BadCommand('Cannot find command %s' % self.name) |
|
3198 logger.info('Found command %s at %s' % (self.name, command)) |
|
3199 self._cmd = command |
|
3200 return command |
|
3201 |
|
3202 def get_url_rev(self): |
|
3203 """ |
|
3204 Returns the correct repository URL and revision by parsing the given |
|
3205 repository URL |
|
3206 """ |
|
3207 url = self.url.split('+', 1)[1] |
|
3208 scheme, netloc, path, query, frag = urlparse.urlsplit(url) |
|
3209 rev = None |
|
3210 if '@' in path: |
|
3211 path, rev = path.rsplit('@', 1) |
|
3212 url = urlparse.urlunsplit((scheme, netloc, path, query, '')) |
|
3213 return url, rev |
|
3214 |
|
3215 def get_info(self, location): |
|
3216 """ |
|
3217 Returns (url, revision), where both are strings |
|
3218 """ |
|
3219 assert not location.rstrip('/').endswith(self.dirname), 'Bad directory: %s' % location |
|
3220 return self.get_url(location), self.get_revision(location) |
|
3221 |
|
3222 def normalize_url(self, url): |
|
3223 """ |
|
3224 Normalize a URL for comparison by unquoting it and removing any trailing slash. |
|
3225 """ |
|
3226 return urllib.unquote(url).rstrip('/') |
|
3227 |
|
3228 def compare_urls(self, url1, url2): |
|
3229 """ |
|
3230 Compare two repo URLs for identity, ignoring incidental differences. |
|
3231 """ |
|
3232 return (self.normalize_url(url1) == self.normalize_url(url2)) |
|
3233 |
|
3234 def parse_vcs_bundle_file(self, content): |
|
3235 """ |
|
3236 Takes the contents of the bundled text file that explains how to revert |
|
3237 the stripped off version control data of the given package and returns |
|
3238 the URL and revision of it. |
|
3239 """ |
|
3240 raise NotImplementedError |
|
3241 |
|
3242 def obtain(self, dest): |
|
3243 """ |
|
3244 Called when installing or updating an editable package, takes the |
|
3245 source path of the checkout. |
|
3246 """ |
|
3247 raise NotImplementedError |
|
3248 |
|
3249 def switch(self, dest, url, rev_options): |
|
3250 """ |
|
3251 Switch the repo at ``dest`` to point to ``URL``. |
|
3252 """ |
|
3253 raise NotImplemented |
|
3254 |
|
3255 def update(self, dest, rev_options): |
|
3256 """ |
|
3257 Update an already-existing repo to the given ``rev_options``. |
|
3258 """ |
|
3259 raise NotImplementedError |
|
3260 |
|
3261 def check_destination(self, dest, url, rev_options, rev_display): |
|
3262 """ |
|
3263 Prepare a location to receive a checkout/clone. |
|
3264 |
|
3265 Return True if the location is ready for (and requires) a |
|
3266 checkout/clone, False otherwise. |
|
3267 """ |
|
3268 checkout = True |
|
3269 prompt = False |
|
3270 if os.path.exists(dest): |
|
3271 checkout = False |
|
3272 if os.path.exists(os.path.join(dest, self.dirname)): |
|
3273 existing_url = self.get_url(dest) |
|
3274 if self.compare_urls(existing_url, url): |
|
3275 logger.info('%s in %s exists, and has correct URL (%s)' |
|
3276 % (self.repo_name.title(), display_path(dest), url)) |
|
3277 logger.notify('Updating %s %s%s' |
|
3278 % (display_path(dest), self.repo_name, rev_display)) |
|
3279 self.update(dest, rev_options) |
|
3280 else: |
|
3281 logger.warn('%s %s in %s exists with URL %s' |
|
3282 % (self.name, self.repo_name, display_path(dest), existing_url)) |
|
3283 prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', ('s', 'i', 'w', 'b')) |
|
3284 else: |
|
3285 logger.warn('Directory %s already exists, and is not a %s %s.' |
|
3286 % (dest, self.name, self.repo_name)) |
|
3287 prompt = ('(i)gnore, (w)ipe, (b)ackup ', ('i', 'w', 'b')) |
|
3288 if prompt: |
|
3289 logger.warn('The plan is to install the %s repository %s' |
|
3290 % (self.name, url)) |
|
3291 response = ask('What to do? %s' % prompt[0], prompt[1]) |
|
3292 |
|
3293 if response == 's': |
|
3294 logger.notify('Switching %s %s to %s%s' |
|
3295 % (self.repo_name, display_path(dest), url, rev_display)) |
|
3296 self.switch(dest, url, rev_options) |
|
3297 elif response == 'i': |
|
3298 # do nothing |
|
3299 pass |
|
3300 elif response == 'w': |
|
3301 logger.warn('Deleting %s' % display_path(dest)) |
|
3302 shutil.rmtree(dest) |
|
3303 checkout = True |
|
3304 elif response == 'b': |
|
3305 dest_dir = backup_dir(dest) |
|
3306 logger.warn('Backing up %s to %s' |
|
3307 % (display_path(dest), dest_dir)) |
|
3308 shutil.move(dest, dest_dir) |
|
3309 checkout = True |
|
3310 return checkout |
|
3311 |
|
3312 def unpack(self, location): |
|
3313 raise NotImplementedError |
|
3314 |
|
3315 def get_src_requirement(self, dist, location, find_tags=False): |
|
3316 raise NotImplementedError |
|
3317 |
|
3318 _svn_xml_url_re = re.compile('url="([^"]+)"') |
|
3319 _svn_rev_re = re.compile('committed-rev="(\d+)"') |
|
3320 _svn_url_re = re.compile(r'URL: (.+)') |
|
3321 _svn_revision_re = re.compile(r'Revision: (.+)') |
|
3322 |
|
3323 class Subversion(VersionControl): |
|
3324 name = 'svn' |
|
3325 dirname = '.svn' |
|
3326 repo_name = 'checkout' |
|
3327 schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https') |
|
3328 bundle_file = 'svn-checkout.txt' |
|
3329 guide = ('# This was an svn checkout; to make it a checkout again run:\n' |
|
3330 'svn checkout --force -r %(rev)s %(url)s .\n') |
|
3331 |
|
3332 def get_info(self, location): |
|
3333 """Returns (url, revision), where both are strings""" |
|
3334 assert not location.rstrip('/').endswith(self.dirname), 'Bad directory: %s' % location |
|
3335 output = call_subprocess( |
|
3336 ['svn', 'info', location], show_stdout=False, extra_environ={'LANG': 'C'}) |
|
3337 match = _svn_url_re.search(output) |
|
3338 if not match: |
|
3339 logger.warn('Cannot determine URL of svn checkout %s' % display_path(location)) |
|
3340 logger.info('Output that cannot be parsed: \n%s' % output) |
|
3341 return None, None |
|
3342 url = match.group(1).strip() |
|
3343 match = _svn_revision_re.search(output) |
|
3344 if not match: |
|
3345 logger.warn('Cannot determine revision of svn checkout %s' % display_path(location)) |
|
3346 logger.info('Output that cannot be parsed: \n%s' % output) |
|
3347 return url, None |
|
3348 return url, match.group(1) |
|
3349 |
|
3350 def get_url(self, location): |
|
3351 return self.get_info(location)[0] |
|
3352 |
|
3353 def get_revision(self, location): |
|
3354 return self.get_info(location)[1] |
|
3355 |
|
3356 def parse_vcs_bundle_file(self, content): |
|
3357 for line in content.splitlines(): |
|
3358 if not line.strip() or line.strip().startswith('#'): |
|
3359 continue |
|
3360 match = re.search(r'^-r\s*([^ ])?', line) |
|
3361 if not match: |
|
3362 return None, None |
|
3363 rev = match.group(1) |
|
3364 rest = line[match.end():].strip().split(None, 1)[0] |
|
3365 return rest, rev |
|
3366 return None, None |
|
3367 |
|
3368 def unpack(self, location): |
|
3369 """Check out the svn repository at the url to the destination location""" |
|
3370 url, rev = self.get_url_rev() |
|
3371 logger.notify('Checking out svn repository %s to %s' % (url, location)) |
|
3372 logger.indent += 2 |
|
3373 try: |
|
3374 if os.path.exists(location): |
|
3375 # Subversion doesn't like to check out over an existing directory |
|
3376 # --force fixes this, but was only added in svn 1.5 |
|
3377 shutil.rmtree(location, onerror=rmtree_errorhandler) |
|
3378 call_subprocess( |
|
3379 ['svn', 'checkout', url, location], |
|
3380 filter_stdout=self._filter, show_stdout=False) |
|
3381 finally: |
|
3382 logger.indent -= 2 |
|
3383 |
|
3384 def export(self, location): |
|
3385 """Export the svn repository at the url to the destination location""" |
|
3386 url, rev = self.get_url_rev() |
|
3387 logger.notify('Checking out svn repository %s to %s' % (url, location)) |
|
3388 logger.indent += 2 |
|
3389 try: |
|
3390 if os.path.exists(location): |
|
3391 # Subversion doesn't like to check out over an existing directory |
|
3392 # --force fixes this, but was only added in svn 1.5 |
|
3393 shutil.rmtree(location, onerror=rmtree_errorhandler) |
|
3394 call_subprocess( |
|
3395 ['svn', 'export', url, location], |
|
3396 filter_stdout=self._filter, show_stdout=False) |
|
3397 finally: |
|
3398 logger.indent -= 2 |
|
3399 |
|
3400 def switch(self, dest, url, rev_options): |
|
3401 call_subprocess( |
|
3402 ['svn', 'switch'] + rev_options + [url, dest]) |
|
3403 |
|
3404 def update(self, dest, rev_options): |
|
3405 call_subprocess( |
|
3406 ['svn', 'update'] + rev_options + [dest]) |
|
3407 |
|
3408 def obtain(self, dest): |
|
3409 url, rev = self.get_url_rev() |
|
3410 if rev: |
|
3411 rev_options = ['-r', rev] |
|
3412 rev_display = ' (to revision %s)' % rev |
|
3413 else: |
|
3414 rev_options = [] |
|
3415 rev_display = '' |
|
3416 if self.check_destination(dest, url, rev_options, rev_display): |
|
3417 logger.notify('Checking out %s%s to %s' |
|
3418 % (url, rev_display, display_path(dest))) |
|
3419 call_subprocess( |
|
3420 ['svn', 'checkout', '-q'] + rev_options + [url, dest]) |
|
3421 |
|
3422 def get_location(self, dist, dependency_links): |
|
3423 egg_fragment_re = re.compile(r'#egg=(.*)$') |
|
3424 for url in dependency_links: |
|
3425 egg_fragment = Link(url).egg_fragment |
|
3426 if not egg_fragment: |
|
3427 continue |
|
3428 if '-' in egg_fragment: |
|
3429 ## FIXME: will this work when a package has - in the name? |
|
3430 key = '-'.join(egg_fragment.split('-')[:-1]).lower() |
|
3431 else: |
|
3432 key = egg_fragment |
|
3433 if key == dist.key: |
|
3434 return url.split('#', 1)[0] |
|
3435 return None |
|
3436 |
|
3437 def get_revision(self, location): |
|
3438 """ |
|
3439 Return the maximum revision for all files under a given location |
|
3440 """ |
|
3441 # Note: taken from setuptools.command.egg_info |
|
3442 revision = 0 |
|
3443 |
|
3444 for base, dirs, files in os.walk(location): |
|
3445 if self.dirname not in dirs: |
|
3446 dirs[:] = [] |
|
3447 continue # no sense walking uncontrolled subdirs |
|
3448 dirs.remove(self.dirname) |
|
3449 entries_fn = os.path.join(base, self.dirname, 'entries') |
|
3450 if not os.path.exists(entries_fn): |
|
3451 ## FIXME: should we warn? |
|
3452 continue |
|
3453 f = open(entries_fn) |
|
3454 data = f.read() |
|
3455 f.close() |
|
3456 |
|
3457 if data.startswith('8') or data.startswith('9') or data.startswith('10'): |
|
3458 data = map(str.splitlines,data.split('\n\x0c\n')) |
|
3459 del data[0][0] # get rid of the '8' |
|
3460 dirurl = data[0][3] |
|
3461 revs = [int(d[9]) for d in data if len(d)>9 and d[9]]+[0] |
|
3462 if revs: |
|
3463 localrev = max(revs) |
|
3464 else: |
|
3465 localrev = 0 |
|
3466 elif data.startswith('<?xml'): |
|
3467 dirurl = _svn_xml_url_re.search(data).group(1) # get repository URL |
|
3468 revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)]+[0] |
|
3469 if revs: |
|
3470 localrev = max(revs) |
|
3471 else: |
|
3472 localrev = 0 |
|
3473 else: |
|
3474 logger.warn("Unrecognized .svn/entries format; skipping %s", base) |
|
3475 dirs[:] = [] |
|
3476 continue |
|
3477 if base == location: |
|
3478 base_url = dirurl+'/' # save the root url |
|
3479 elif not dirurl.startswith(base_url): |
|
3480 dirs[:] = [] |
|
3481 continue # not part of the same svn tree, skip it |
|
3482 revision = max(revision, localrev) |
|
3483 return revision |
|
3484 |
|
3485 def get_url(self, location): |
|
3486 # In cases where the source is in a subdirectory, not alongside setup.py |
|
3487 # we have to look up in the location until we find a real setup.py |
|
3488 orig_location = location |
|
3489 while not os.path.exists(os.path.join(location, 'setup.py')): |
|
3490 last_location = location |
|
3491 location = os.path.dirname(location) |
|
3492 if location == last_location: |
|
3493 # We've traversed up to the root of the filesystem without finding setup.py |
|
3494 logger.warn("Could not find setup.py for directory %s (tried all parent directories)" |
|
3495 % orig_location) |
|
3496 return None |
|
3497 f = open(os.path.join(location, self.dirname, 'entries')) |
|
3498 data = f.read() |
|
3499 f.close() |
|
3500 if data.startswith('8') or data.startswith('9') or data.startswith('10'): |
|
3501 data = map(str.splitlines,data.split('\n\x0c\n')) |
|
3502 del data[0][0] # get rid of the '8' |
|
3503 return data[0][3] |
|
3504 elif data.startswith('<?xml'): |
|
3505 match = _svn_xml_url_re.search(data) |
|
3506 if not match: |
|
3507 raise ValueError('Badly formatted data: %r' % data) |
|
3508 return match.group(1) # get repository URL |
|
3509 else: |
|
3510 logger.warn("Unrecognized .svn/entries format in %s" % location) |
|
3511 # Or raise exception? |
|
3512 return None |
|
3513 |
|
3514 def get_tag_revs(self, svn_tag_url): |
|
3515 stdout = call_subprocess( |
|
3516 ['svn', 'ls', '-v', svn_tag_url], show_stdout=False) |
|
3517 results = [] |
|
3518 for line in stdout.splitlines(): |
|
3519 parts = line.split() |
|
3520 rev = int(parts[0]) |
|
3521 tag = parts[-1].strip('/') |
|
3522 results.append((tag, rev)) |
|
3523 return results |
|
3524 |
|
3525 def find_tag_match(self, rev, tag_revs): |
|
3526 best_match_rev = None |
|
3527 best_tag = None |
|
3528 for tag, tag_rev in tag_revs: |
|
3529 if (tag_rev > rev and |
|
3530 (best_match_rev is None or best_match_rev > tag_rev)): |
|
3531 # FIXME: Is best_match > tag_rev really possible? |
|
3532 # or is it a sign something is wacky? |
|
3533 best_match_rev = tag_rev |
|
3534 best_tag = tag |
|
3535 return best_tag |
|
3536 |
|
3537 def get_src_requirement(self, dist, location, find_tags=False): |
|
3538 repo = self.get_url(location) |
|
3539 if repo is None: |
|
3540 return None |
|
3541 parts = repo.split('/') |
|
3542 ## FIXME: why not project name? |
|
3543 egg_project_name = dist.egg_name().split('-', 1)[0] |
|
3544 rev = self.get_revision(location) |
|
3545 if parts[-2] in ('tags', 'tag'): |
|
3546 # It's a tag, perfect! |
|
3547 full_egg_name = '%s-%s' % (egg_project_name, parts[-1]) |
|
3548 elif parts[-2] in ('branches', 'branch'): |
|
3549 # It's a branch :( |
|
3550 full_egg_name = '%s-%s-r%s' % (dist.egg_name(), parts[-1], rev) |
|
3551 elif parts[-1] == 'trunk': |
|
3552 # Trunk :-/ |
|
3553 full_egg_name = '%s-dev_r%s' % (dist.egg_name(), rev) |
|
3554 if find_tags: |
|
3555 tag_url = '/'.join(parts[:-1]) + '/tags' |
|
3556 tag_revs = self.get_tag_revs(tag_url) |
|
3557 match = self.find_tag_match(rev, tag_revs) |
|
3558 if match: |
|
3559 logger.notify('trunk checkout %s seems to be equivalent to tag %s' % match) |
|
3560 repo = '%s/%s' % (tag_url, match) |
|
3561 full_egg_name = '%s-%s' % (egg_project_name, match) |
|
3562 else: |
|
3563 # Don't know what it is |
|
3564 logger.warn('svn URL does not fit normal structure (tags/branches/trunk): %s' % repo) |
|
3565 full_egg_name = '%s-dev_r%s' % (egg_project_name, rev) |
|
3566 return 'svn+%s@%s#egg=%s' % (repo, rev, full_egg_name) |
|
3567 |
|
3568 vcs.register(Subversion) |
|
3569 |
|
3570 |
|
3571 class Git(VersionControl): |
|
3572 name = 'git' |
|
3573 dirname = '.git' |
|
3574 repo_name = 'clone' |
|
3575 schemes = ('git', 'git+http', 'git+ssh', 'git+git') |
|
3576 bundle_file = 'git-clone.txt' |
|
3577 guide = ('# This was a Git repo; to make it a repo again run:\n' |
|
3578 'git init\ngit remote add origin %(url)s -f\ngit checkout %(rev)s\n') |
|
3579 |
|
3580 def parse_vcs_bundle_file(self, content): |
|
3581 url = rev = None |
|
3582 for line in content.splitlines(): |
|
3583 if not line.strip() or line.strip().startswith('#'): |
|
3584 continue |
|
3585 url_match = re.search(r'git\s*remote\s*add\s*origin(.*)\s*-f', line) |
|
3586 if url_match: |
|
3587 url = url_match.group(1).strip() |
|
3588 rev_match = re.search(r'^git\s*checkout\s*-q\s*(.*)\s*', line) |
|
3589 if rev_match: |
|
3590 rev = rev_match.group(1).strip() |
|
3591 if url and rev: |
|
3592 return url, rev |
|
3593 return None, None |
|
3594 |
|
3595 def unpack(self, location): |
|
3596 """Clone the Git repository at the url to the destination location""" |
|
3597 url, rev = self.get_url_rev() |
|
3598 logger.notify('Cloning Git repository %s to %s' % (url, location)) |
|
3599 logger.indent += 2 |
|
3600 try: |
|
3601 if os.path.exists(location): |
|
3602 os.rmdir(location) |
|
3603 call_subprocess( |
|
3604 [self.cmd, 'clone', url, location], |
|
3605 filter_stdout=self._filter, show_stdout=False) |
|
3606 finally: |
|
3607 logger.indent -= 2 |
|
3608 |
|
3609 def export(self, location): |
|
3610 """Export the Git repository at the url to the destination location""" |
|
3611 temp_dir = tempfile.mkdtemp('-export', 'pip-') |
|
3612 self.unpack(temp_dir) |
|
3613 try: |
|
3614 if not location.endswith('/'): |
|
3615 location = location + '/' |
|
3616 call_subprocess( |
|
3617 [self.cmd, 'checkout-index', '-a', '-f', '--prefix', location], |
|
3618 filter_stdout=self._filter, show_stdout=False, cwd=temp_dir) |
|
3619 finally: |
|
3620 shutil.rmtree(temp_dir) |
|
3621 |
|
3622 def check_rev_options(self, rev, dest, rev_options): |
|
3623 """Check the revision options before checkout to compensate that tags |
|
3624 and branches may need origin/ as a prefix""" |
|
3625 if rev is None: |
|
3626 # bail and use preset |
|
3627 return rev_options |
|
3628 revisions = self.get_tag_revs(dest) |
|
3629 revisions.update(self.get_branch_revs(dest)) |
|
3630 if rev in revisions: |
|
3631 # if rev is a sha |
|
3632 return [rev] |
|
3633 inverse_revisions = dict((v,k) for k, v in revisions.iteritems()) |
|
3634 if rev not in inverse_revisions: # is rev a name or tag? |
|
3635 origin_rev = 'origin/%s' % rev |
|
3636 if origin_rev in inverse_revisions: |
|
3637 rev = inverse_revisions[origin_rev] |
|
3638 else: |
|
3639 logger.warn("Could not find a tag or branch '%s', assuming commit." % rev) |
|
3640 return [rev] |
|
3641 |
|
3642 def switch(self, dest, url, rev_options): |
|
3643 |
|
3644 call_subprocess( |
|
3645 [self.cmd, 'config', 'remote.origin.url', url], cwd=dest) |
|
3646 call_subprocess( |
|
3647 [self.cmd, 'checkout', '-q'] + rev_options, cwd=dest) |
|
3648 |
|
3649 def update(self, dest, rev_options): |
|
3650 call_subprocess([self.cmd, 'fetch', '-q'], cwd=dest) |
|
3651 call_subprocess( |
|
3652 [self.cmd, 'checkout', '-q', '-f'] + rev_options, cwd=dest) |
|
3653 |
|
3654 def obtain(self, dest): |
|
3655 url, rev = self.get_url_rev() |
|
3656 if rev: |
|
3657 rev_options = [rev] |
|
3658 rev_display = ' (to %s)' % rev |
|
3659 else: |
|
3660 rev_options = ['origin/master'] |
|
3661 rev_display = '' |
|
3662 if self.check_destination(dest, url, rev_options, rev_display): |
|
3663 logger.notify('Cloning %s%s to %s' % (url, rev_display, display_path(dest))) |
|
3664 call_subprocess( |
|
3665 [self.cmd, 'clone', '-q', url, dest]) |
|
3666 rev_options = self.check_rev_options(rev, dest, rev_options) |
|
3667 call_subprocess( |
|
3668 [self.cmd, 'checkout', '-q'] + rev_options, cwd=dest) |
|
3669 |
|
3670 def get_url(self, location): |
|
3671 url = call_subprocess( |
|
3672 [self.cmd, 'config', 'remote.origin.url'], |
|
3673 show_stdout=False, cwd=location) |
|
3674 return url.strip() |
|
3675 |
|
3676 def get_revision(self, location): |
|
3677 current_rev = call_subprocess( |
|
3678 [self.cmd, 'rev-parse', 'HEAD'], show_stdout=False, cwd=location) |
|
3679 return current_rev.strip() |
|
3680 |
|
3681 def get_tag_revs(self, location): |
|
3682 tags = call_subprocess( |
|
3683 [self.cmd, 'tag'], show_stdout=False, cwd=location) |
|
3684 tag_revs = [] |
|
3685 for line in tags.splitlines(): |
|
3686 tag = line.strip() |
|
3687 rev = call_subprocess( |
|
3688 [self.cmd, 'rev-parse', tag], show_stdout=False, cwd=location) |
|
3689 tag_revs.append((rev.strip(), tag)) |
|
3690 tag_revs = dict(tag_revs) |
|
3691 return tag_revs |
|
3692 |
|
3693 def get_branch_revs(self, location): |
|
3694 branches = call_subprocess( |
|
3695 [self.cmd, 'branch', '-r'], show_stdout=False, cwd=location) |
|
3696 branch_revs = [] |
|
3697 for line in branches.splitlines(): |
|
3698 line = line.split('->')[0].strip() |
|
3699 branch = "".join([b for b in line.split() if b != '*']) |
|
3700 rev = call_subprocess( |
|
3701 [self.cmd, 'rev-parse', branch], show_stdout=False, cwd=location) |
|
3702 branch_revs.append((rev.strip(), branch)) |
|
3703 branch_revs = dict(branch_revs) |
|
3704 return branch_revs |
|
3705 |
|
3706 def get_src_requirement(self, dist, location, find_tags): |
|
3707 repo = self.get_url(location) |
|
3708 if not repo.lower().startswith('git:'): |
|
3709 repo = 'git+' + repo |
|
3710 egg_project_name = dist.egg_name().split('-', 1)[0] |
|
3711 if not repo: |
|
3712 return None |
|
3713 current_rev = self.get_revision(location) |
|
3714 tag_revs = self.get_tag_revs(location) |
|
3715 branch_revs = self.get_branch_revs(location) |
|
3716 |
|
3717 if current_rev in tag_revs: |
|
3718 # It's a tag |
|
3719 full_egg_name = '%s-%s' % (egg_project_name, tag_revs[current_rev]) |
|
3720 elif (current_rev in branch_revs and |
|
3721 branch_revs[current_rev] != 'origin/master'): |
|
3722 # It's the head of a branch |
|
3723 full_egg_name = '%s-%s' % (dist.egg_name(), |
|
3724 branch_revs[current_rev].replace('origin/', '')) |
|
3725 else: |
|
3726 full_egg_name = '%s-dev' % dist.egg_name() |
|
3727 |
|
3728 return '%s@%s#egg=%s' % (repo, current_rev, full_egg_name) |
|
3729 |
|
3730 def get_url_rev(self): |
|
3731 """ |
|
3732 Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'. |
|
3733 That's required because although they use SSH they sometimes doesn't |
|
3734 work with a ssh:// scheme (e.g. Github). But we need a scheme for |
|
3735 parsing. Hence we remove it again afterwards and return it as a stub. |
|
3736 """ |
|
3737 if not '://' in self.url: |
|
3738 self.url = self.url.replace('git+', 'git+ssh://') |
|
3739 url, rev = super(Git, self).get_url_rev() |
|
3740 url = url.replace('ssh://', '') |
|
3741 return url, rev |
|
3742 return super(Git, self).get_url_rev() |
|
3743 |
|
3744 vcs.register(Git) |
|
3745 |
|
3746 class Mercurial(VersionControl): |
|
3747 name = 'hg' |
|
3748 dirname = '.hg' |
|
3749 repo_name = 'clone' |
|
3750 schemes = ('hg', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http') |
|
3751 bundle_file = 'hg-clone.txt' |
|
3752 guide = ('# This was a Mercurial repo; to make it a repo again run:\n' |
|
3753 'hg init\nhg pull %(url)s\nhg update -r %(rev)s\n') |
|
3754 |
|
3755 def parse_vcs_bundle_file(self, content): |
|
3756 url = rev = None |
|
3757 for line in content.splitlines(): |
|
3758 if not line.strip() or line.strip().startswith('#'): |
|
3759 continue |
|
3760 url_match = re.search(r'hg\s*pull\s*(.*)\s*', line) |
|
3761 if url_match: |
|
3762 url = url_match.group(1).strip() |
|
3763 rev_match = re.search(r'^hg\s*update\s*-r\s*(.*)\s*', line) |
|
3764 if rev_match: |
|
3765 rev = rev_match.group(1).strip() |
|
3766 if url and rev: |
|
3767 return url, rev |
|
3768 return None, None |
|
3769 |
|
3770 def unpack(self, location): |
|
3771 """Clone the Hg repository at the url to the destination location""" |
|
3772 url, rev = self.get_url_rev() |
|
3773 logger.notify('Cloning Mercurial repository %s to %s' % (url, location)) |
|
3774 logger.indent += 2 |
|
3775 try: |
|
3776 if os.path.exists(location): |
|
3777 os.rmdir(location) |
|
3778 call_subprocess( |
|
3779 ['hg', 'clone', url, location], |
|
3780 filter_stdout=self._filter, show_stdout=False) |
|
3781 finally: |
|
3782 logger.indent -= 2 |
|
3783 |
|
3784 def export(self, location): |
|
3785 """Export the Hg repository at the url to the destination location""" |
|
3786 temp_dir = tempfile.mkdtemp('-export', 'pip-') |
|
3787 self.unpack(temp_dir) |
|
3788 try: |
|
3789 call_subprocess( |
|
3790 ['hg', 'archive', location], |
|
3791 filter_stdout=self._filter, show_stdout=False, cwd=temp_dir) |
|
3792 finally: |
|
3793 shutil.rmtree(temp_dir) |
|
3794 |
|
3795 def switch(self, dest, url, rev_options): |
|
3796 repo_config = os.path.join(dest, self.dirname, 'hgrc') |
|
3797 config = ConfigParser.SafeConfigParser() |
|
3798 try: |
|
3799 config.read(repo_config) |
|
3800 config.set('paths', 'default', url) |
|
3801 config_file = open(repo_config, 'w') |
|
3802 config.write(config_file) |
|
3803 config_file.close() |
|
3804 except (OSError, ConfigParser.NoSectionError), e: |
|
3805 logger.warn( |
|
3806 'Could not switch Mercurial repository to %s: %s' |
|
3807 % (url, e)) |
|
3808 else: |
|
3809 call_subprocess(['hg', 'update', '-q'] + rev_options, cwd=dest) |
|
3810 |
|
3811 def update(self, dest, rev_options): |
|
3812 call_subprocess(['hg', 'pull', '-q'], cwd=dest) |
|
3813 call_subprocess( |
|
3814 ['hg', 'update', '-q'] + rev_options, cwd=dest) |
|
3815 |
|
3816 def obtain(self, dest): |
|
3817 url, rev = self.get_url_rev() |
|
3818 if rev: |
|
3819 rev_options = [rev] |
|
3820 rev_display = ' (to revision %s)' % rev |
|
3821 else: |
|
3822 rev_options = [] |
|
3823 rev_display = '' |
|
3824 if self.check_destination(dest, url, rev_options, rev_display): |
|
3825 logger.notify('Cloning hg %s%s to %s' |
|
3826 % (url, rev_display, display_path(dest))) |
|
3827 call_subprocess(['hg', 'clone', '-q', url, dest]) |
|
3828 call_subprocess(['hg', 'update', '-q'] + rev_options, cwd=dest) |
|
3829 |
|
3830 def get_url(self, location): |
|
3831 url = call_subprocess( |
|
3832 ['hg', 'showconfig', 'paths.default'], |
|
3833 show_stdout=False, cwd=location).strip() |
|
3834 if url.startswith('/') or url.startswith('\\'): |
|
3835 url = filename_to_url(url) |
|
3836 return url.strip() |
|
3837 |
|
3838 def get_tag_revs(self, location): |
|
3839 tags = call_subprocess( |
|
3840 ['hg', 'tags'], show_stdout=False, cwd=location) |
|
3841 tag_revs = [] |
|
3842 for line in tags.splitlines(): |
|
3843 tags_match = re.search(r'([\w\d\.-]+)\s*([\d]+):.*$', line) |
|
3844 if tags_match: |
|
3845 tag = tags_match.group(1) |
|
3846 rev = tags_match.group(2) |
|
3847 tag_revs.append((rev.strip(), tag.strip())) |
|
3848 return dict(tag_revs) |
|
3849 |
|
3850 def get_branch_revs(self, location): |
|
3851 branches = call_subprocess( |
|
3852 ['hg', 'branches'], show_stdout=False, cwd=location) |
|
3853 branch_revs = [] |
|
3854 for line in branches.splitlines(): |
|
3855 branches_match = re.search(r'([\w\d\.-]+)\s*([\d]+):.*$', line) |
|
3856 if branches_match: |
|
3857 branch = branches_match.group(1) |
|
3858 rev = branches_match.group(2) |
|
3859 branch_revs.append((rev.strip(), branch.strip())) |
|
3860 return dict(branch_revs) |
|
3861 |
|
3862 def get_revision(self, location): |
|
3863 current_revision = call_subprocess( |
|
3864 ['hg', 'parents', '--template={rev}'], |
|
3865 show_stdout=False, cwd=location).strip() |
|
3866 return current_revision |
|
3867 |
|
3868 def get_revision_hash(self, location): |
|
3869 current_rev_hash = call_subprocess( |
|
3870 ['hg', 'parents', '--template={node}'], |
|
3871 show_stdout=False, cwd=location).strip() |
|
3872 return current_rev_hash |
|
3873 |
|
3874 def get_src_requirement(self, dist, location, find_tags): |
|
3875 repo = self.get_url(location) |
|
3876 if not repo.lower().startswith('hg:'): |
|
3877 repo = 'hg+' + repo |
|
3878 egg_project_name = dist.egg_name().split('-', 1)[0] |
|
3879 if not repo: |
|
3880 return None |
|
3881 current_rev = self.get_revision(location) |
|
3882 current_rev_hash = self.get_revision_hash(location) |
|
3883 tag_revs = self.get_tag_revs(location) |
|
3884 branch_revs = self.get_branch_revs(location) |
|
3885 if current_rev in tag_revs: |
|
3886 # It's a tag |
|
3887 full_egg_name = '%s-%s' % (egg_project_name, tag_revs[current_rev]) |
|
3888 elif current_rev in branch_revs: |
|
3889 # It's the tip of a branch |
|
3890 full_egg_name = '%s-%s' % (dist.egg_name(), branch_revs[current_rev]) |
|
3891 else: |
|
3892 full_egg_name = '%s-dev' % dist.egg_name() |
|
3893 return '%s@%s#egg=%s' % (repo, current_rev_hash, full_egg_name) |
|
3894 |
|
3895 vcs.register(Mercurial) |
|
3896 |
|
3897 |
|
3898 class Bazaar(VersionControl): |
|
3899 name = 'bzr' |
|
3900 dirname = '.bzr' |
|
3901 repo_name = 'branch' |
|
3902 bundle_file = 'bzr-branch.txt' |
|
3903 schemes = ('bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp') |
|
3904 guide = ('# This was a Bazaar branch; to make it a branch again run:\n' |
|
3905 'bzr branch -r %(rev)s %(url)s .\n') |
|
3906 |
|
3907 def parse_vcs_bundle_file(self, content): |
|
3908 url = rev = None |
|
3909 for line in content.splitlines(): |
|
3910 if not line.strip() or line.strip().startswith('#'): |
|
3911 continue |
|
3912 match = re.search(r'^bzr\s*branch\s*-r\s*(\d*)', line) |
|
3913 if match: |
|
3914 rev = match.group(1).strip() |
|
3915 url = line[match.end():].strip().split(None, 1)[0] |
|
3916 if url and rev: |
|
3917 return url, rev |
|
3918 return None, None |
|
3919 |
|
3920 def unpack(self, location): |
|
3921 """Get the bzr branch at the url to the destination location""" |
|
3922 url, rev = self.get_url_rev() |
|
3923 logger.notify('Checking out bzr repository %s to %s' % (url, location)) |
|
3924 logger.indent += 2 |
|
3925 try: |
|
3926 if os.path.exists(location): |
|
3927 os.rmdir(location) |
|
3928 call_subprocess( |
|
3929 [self.cmd, 'branch', url, location], |
|
3930 filter_stdout=self._filter, show_stdout=False) |
|
3931 finally: |
|
3932 logger.indent -= 2 |
|
3933 |
|
3934 def export(self, location): |
|
3935 """Export the Bazaar repository at the url to the destination location""" |
|
3936 temp_dir = tempfile.mkdtemp('-export', 'pip-') |
|
3937 self.unpack(temp_dir) |
|
3938 if os.path.exists(location): |
|
3939 # Remove the location to make sure Bazaar can export it correctly |
|
3940 shutil.rmtree(location, onerror=rmtree_errorhandler) |
|
3941 try: |
|
3942 call_subprocess([self.cmd, 'export', location], cwd=temp_dir, |
|
3943 filter_stdout=self._filter, show_stdout=False) |
|
3944 finally: |
|
3945 shutil.rmtree(temp_dir) |
|
3946 |
|
3947 def switch(self, dest, url, rev_options): |
|
3948 call_subprocess([self.cmd, 'switch', url], cwd=dest) |
|
3949 |
|
3950 def update(self, dest, rev_options): |
|
3951 call_subprocess( |
|
3952 [self.cmd, 'pull', '-q'] + rev_options, cwd=dest) |
|
3953 |
|
3954 def obtain(self, dest): |
|
3955 url, rev = self.get_url_rev() |
|
3956 if rev: |
|
3957 rev_options = ['-r', rev] |
|
3958 rev_display = ' (to revision %s)' % rev |
|
3959 else: |
|
3960 rev_options = [] |
|
3961 rev_display = '' |
|
3962 if self.check_destination(dest, url, rev_options, rev_display): |
|
3963 logger.notify('Checking out %s%s to %s' |
|
3964 % (url, rev_display, display_path(dest))) |
|
3965 call_subprocess( |
|
3966 [self.cmd, 'branch', '-q'] + rev_options + [url, dest]) |
|
3967 |
|
3968 def get_url_rev(self): |
|
3969 # hotfix the URL scheme after removing bzr+ from bzr+ssh:// readd it |
|
3970 url, rev = super(Bazaar, self).get_url_rev() |
|
3971 if url.startswith('ssh://'): |
|
3972 url = 'bzr+' + url |
|
3973 return url, rev |
|
3974 |
|
3975 def get_url(self, location): |
|
3976 urls = call_subprocess( |
|
3977 [self.cmd, 'info'], show_stdout=False, cwd=location) |
|
3978 for line in urls.splitlines(): |
|
3979 line = line.strip() |
|
3980 for x in ('checkout of branch: ', |
|
3981 'parent branch: '): |
|
3982 if line.startswith(x): |
|
3983 return line.split(x)[1] |
|
3984 return None |
|
3985 |
|
3986 def get_revision(self, location): |
|
3987 revision = call_subprocess( |
|
3988 [self.cmd, 'revno'], show_stdout=False, cwd=location) |
|
3989 return revision.splitlines()[-1] |
|
3990 |
|
3991 def get_tag_revs(self, location): |
|
3992 tags = call_subprocess( |
|
3993 [self.cmd, 'tags'], show_stdout=False, cwd=location) |
|
3994 tag_revs = [] |
|
3995 for line in tags.splitlines(): |
|
3996 tags_match = re.search(r'([.\w-]+)\s*(.*)$', line) |
|
3997 if tags_match: |
|
3998 tag = tags_match.group(1) |
|
3999 rev = tags_match.group(2) |
|
4000 tag_revs.append((rev.strip(), tag.strip())) |
|
4001 return dict(tag_revs) |
|
4002 |
|
4003 def get_src_requirement(self, dist, location, find_tags): |
|
4004 repo = self.get_url(location) |
|
4005 if not repo.lower().startswith('bzr:'): |
|
4006 repo = 'bzr+' + repo |
|
4007 egg_project_name = dist.egg_name().split('-', 1)[0] |
|
4008 if not repo: |
|
4009 return None |
|
4010 current_rev = self.get_revision(location) |
|
4011 tag_revs = self.get_tag_revs(location) |
|
4012 |
|
4013 if current_rev in tag_revs: |
|
4014 # It's a tag |
|
4015 tag = tag_revs.get(current_rev, current_rev) |
|
4016 full_egg_name = '%s-%s' % (egg_project_name, tag_revs[current_rev]) |
|
4017 else: |
|
4018 full_egg_name = '%s-dev_r%s' % (dist.egg_name(), current_rev) |
|
4019 return '%s@%s#egg=%s' % (repo, current_rev, full_egg_name) |
|
4020 |
|
4021 vcs.register(Bazaar) |
|
4022 |
|
4023 def get_src_requirement(dist, location, find_tags): |
|
4024 version_control = vcs.get_backend_from_location(location) |
|
4025 if version_control: |
|
4026 return version_control().get_src_requirement(dist, location, find_tags) |
|
4027 logger.warn('cannot determine version of editable source in %s (is not SVN checkout, Git clone, Mercurial clone or Bazaar branch)' % location) |
|
4028 return dist.as_requirement() |
|
4029 |
|
4030 ############################################################ |
|
4031 ## Requirement files |
|
4032 |
|
4033 _scheme_re = re.compile(r'^(http|https|file):', re.I) |
|
4034 _url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) |
|
4035 def get_file_content(url, comes_from=None): |
|
4036 """Gets the content of a file; it may be a filename, file: URL, or |
|
4037 http: URL. Returns (location, content)""" |
|
4038 match = _scheme_re.search(url) |
|
4039 if match: |
|
4040 scheme = match.group(1).lower() |
|
4041 if (scheme == 'file' and comes_from |
|
4042 and comes_from.startswith('http')): |
|
4043 raise InstallationError( |
|
4044 'Requirements file %s references URL %s, which is local' |
|
4045 % (comes_from, url)) |
|
4046 if scheme == 'file': |
|
4047 path = url.split(':', 1)[1] |
|
4048 path = path.replace('\\', '/') |
|
4049 match = _url_slash_drive_re.match(path) |
|
4050 if match: |
|
4051 path = match.group(1) + ':' + path.split('|', 1)[1] |
|
4052 path = urllib.unquote(path) |
|
4053 if path.startswith('/'): |
|
4054 path = '/' + path.lstrip('/') |
|
4055 url = path |
|
4056 else: |
|
4057 ## FIXME: catch some errors |
|
4058 resp = urllib2.urlopen(url) |
|
4059 return resp.geturl(), resp.read() |
|
4060 f = open(url) |
|
4061 content = f.read() |
|
4062 f.close() |
|
4063 return url, content |
|
4064 |
|
4065 def parse_requirements(filename, finder=None, comes_from=None, options=None): |
|
4066 skip_match = None |
|
4067 skip_regex = options.skip_requirements_regex |
|
4068 if skip_regex: |
|
4069 skip_match = re.compile(skip_regex) |
|
4070 filename, content = get_file_content(filename, comes_from=comes_from) |
|
4071 for line_number, line in enumerate(content.splitlines()): |
|
4072 line_number += 1 |
|
4073 line = line.strip() |
|
4074 if not line or line.startswith('#'): |
|
4075 continue |
|
4076 if skip_match and skip_match.search(line): |
|
4077 continue |
|
4078 if line.startswith('-r') or line.startswith('--requirement'): |
|
4079 if line.startswith('-r'): |
|
4080 req_url = line[2:].strip() |
|
4081 else: |
|
4082 req_url = line[len('--requirement'):].strip().strip('=') |
|
4083 if _scheme_re.search(filename): |
|
4084 # Relative to a URL |
|
4085 req_url = urlparse.urljoin(filename, url) |
|
4086 elif not _scheme_re.search(req_url): |
|
4087 req_url = os.path.join(os.path.dirname(filename), req_url) |
|
4088 for item in parse_requirements(req_url, finder, comes_from=filename, options=options): |
|
4089 yield item |
|
4090 elif line.startswith('-Z') or line.startswith('--always-unzip'): |
|
4091 # No longer used, but previously these were used in |
|
4092 # requirement files, so we'll ignore. |
|
4093 pass |
|
4094 elif finder and line.startswith('-f') or line.startswith('--find-links'): |
|
4095 if line.startswith('-f'): |
|
4096 line = line[2:].strip() |
|
4097 else: |
|
4098 line = line[len('--find-links'):].strip().lstrip('=') |
|
4099 ## FIXME: it would be nice to keep track of the source of |
|
4100 ## the find_links: |
|
4101 finder.find_links.append(line) |
|
4102 elif line.startswith('-i') or line.startswith('--index-url'): |
|
4103 if line.startswith('-i'): |
|
4104 line = line[2:].strip() |
|
4105 else: |
|
4106 line = line[len('--index-url'):].strip().lstrip('=') |
|
4107 finder.index_urls = [line] |
|
4108 elif line.startswith('--extra-index-url'): |
|
4109 line = line[len('--extra-index-url'):].strip().lstrip('=') |
|
4110 finder.index_urls.append(line) |
|
4111 else: |
|
4112 comes_from = '-r %s (line %s)' % (filename, line_number) |
|
4113 if line.startswith('-e') or line.startswith('--editable'): |
|
4114 if line.startswith('-e'): |
|
4115 line = line[2:].strip() |
|
4116 else: |
|
4117 line = line[len('--editable'):].strip() |
|
4118 req = InstallRequirement.from_editable( |
|
4119 line, comes_from=comes_from, default_vcs=options.default_vcs) |
|
4120 else: |
|
4121 req = InstallRequirement.from_line(line, comes_from) |
|
4122 yield req |
|
4123 |
|
4124 ############################################################ |
|
4125 ## Logging |
|
4126 |
|
4127 |
|
4128 |
|
4129 class Logger(object): |
|
4130 |
|
4131 """ |
|
4132 Logging object for use in command-line script. Allows ranges of |
|
4133 levels, to avoid some redundancy of displayed information. |
|
4134 """ |
|
4135 |
|
4136 VERBOSE_DEBUG = logging.DEBUG-1 |
|
4137 DEBUG = logging.DEBUG |
|
4138 INFO = logging.INFO |
|
4139 NOTIFY = (logging.INFO+logging.WARN)/2 |
|
4140 WARN = WARNING = logging.WARN |
|
4141 ERROR = logging.ERROR |
|
4142 FATAL = logging.FATAL |
|
4143 |
|
4144 LEVELS = [VERBOSE_DEBUG, DEBUG, INFO, NOTIFY, WARN, ERROR, FATAL] |
|
4145 |
|
4146 def __init__(self, consumers): |
|
4147 self.consumers = consumers |
|
4148 self.indent = 0 |
|
4149 self.explicit_levels = False |
|
4150 self.in_progress = None |
|
4151 self.in_progress_hanging = False |
|
4152 |
|
4153 def debug(self, msg, *args, **kw): |
|
4154 self.log(self.DEBUG, msg, *args, **kw) |
|
4155 def info(self, msg, *args, **kw): |
|
4156 self.log(self.INFO, msg, *args, **kw) |
|
4157 def notify(self, msg, *args, **kw): |
|
4158 self.log(self.NOTIFY, msg, *args, **kw) |
|
4159 def warn(self, msg, *args, **kw): |
|
4160 self.log(self.WARN, msg, *args, **kw) |
|
4161 def error(self, msg, *args, **kw): |
|
4162 self.log(self.WARN, msg, *args, **kw) |
|
4163 def fatal(self, msg, *args, **kw): |
|
4164 self.log(self.FATAL, msg, *args, **kw) |
|
4165 def log(self, level, msg, *args, **kw): |
|
4166 if args: |
|
4167 if kw: |
|
4168 raise TypeError( |
|
4169 "You may give positional or keyword arguments, not both") |
|
4170 args = args or kw |
|
4171 rendered = None |
|
4172 for consumer_level, consumer in self.consumers: |
|
4173 if self.level_matches(level, consumer_level): |
|
4174 if (self.in_progress_hanging |
|
4175 and consumer in (sys.stdout, sys.stderr)): |
|
4176 self.in_progress_hanging = False |
|
4177 sys.stdout.write('\n') |
|
4178 sys.stdout.flush() |
|
4179 if rendered is None: |
|
4180 if args: |
|
4181 rendered = msg % args |
|
4182 else: |
|
4183 rendered = msg |
|
4184 rendered = ' '*self.indent + rendered |
|
4185 if self.explicit_levels: |
|
4186 ## FIXME: should this be a name, not a level number? |
|
4187 rendered = '%02i %s' % (level, rendered) |
|
4188 if hasattr(consumer, 'write'): |
|
4189 consumer.write(rendered+'\n') |
|
4190 else: |
|
4191 consumer(rendered) |
|
4192 |
|
4193 def start_progress(self, msg): |
|
4194 assert not self.in_progress, ( |
|
4195 "Tried to start_progress(%r) while in_progress %r" |
|
4196 % (msg, self.in_progress)) |
|
4197 if self.level_matches(self.NOTIFY, self._stdout_level()): |
|
4198 sys.stdout.write(' '*self.indent + msg) |
|
4199 sys.stdout.flush() |
|
4200 self.in_progress_hanging = True |
|
4201 else: |
|
4202 self.in_progress_hanging = False |
|
4203 self.in_progress = msg |
|
4204 self.last_message = None |
|
4205 |
|
4206 def end_progress(self, msg='done.'): |
|
4207 assert self.in_progress, ( |
|
4208 "Tried to end_progress without start_progress") |
|
4209 if self.stdout_level_matches(self.NOTIFY): |
|
4210 if not self.in_progress_hanging: |
|
4211 # Some message has been printed out since start_progress |
|
4212 sys.stdout.write('...' + self.in_progress + msg + '\n') |
|
4213 sys.stdout.flush() |
|
4214 else: |
|
4215 # These erase any messages shown with show_progress (besides .'s) |
|
4216 logger.show_progress('') |
|
4217 logger.show_progress('') |
|
4218 sys.stdout.write(msg + '\n') |
|
4219 sys.stdout.flush() |
|
4220 self.in_progress = None |
|
4221 self.in_progress_hanging = False |
|
4222 |
|
4223 def show_progress(self, message=None): |
|
4224 """If we are in a progress scope, and no log messages have been |
|
4225 shown, write out another '.'""" |
|
4226 if self.in_progress_hanging: |
|
4227 if message is None: |
|
4228 sys.stdout.write('.') |
|
4229 sys.stdout.flush() |
|
4230 else: |
|
4231 if self.last_message: |
|
4232 padding = ' ' * max(0, len(self.last_message)-len(message)) |
|
4233 else: |
|
4234 padding = '' |
|
4235 sys.stdout.write('\r%s%s%s%s' % (' '*self.indent, self.in_progress, message, padding)) |
|
4236 sys.stdout.flush() |
|
4237 self.last_message = message |
|
4238 |
|
4239 def stdout_level_matches(self, level): |
|
4240 """Returns true if a message at this level will go to stdout""" |
|
4241 return self.level_matches(level, self._stdout_level()) |
|
4242 |
|
4243 def _stdout_level(self): |
|
4244 """Returns the level that stdout runs at""" |
|
4245 for level, consumer in self.consumers: |
|
4246 if consumer is sys.stdout: |
|
4247 return level |
|
4248 return self.FATAL |
|
4249 |
|
4250 def level_matches(self, level, consumer_level): |
|
4251 """ |
|
4252 >>> l = Logger() |
|
4253 >>> l.level_matches(3, 4) |
|
4254 False |
|
4255 >>> l.level_matches(3, 2) |
|
4256 True |
|
4257 >>> l.level_matches(slice(None, 3), 3) |
|
4258 False |
|
4259 >>> l.level_matches(slice(None, 3), 2) |
|
4260 True |
|
4261 >>> l.level_matches(slice(1, 3), 1) |
|
4262 True |
|
4263 >>> l.level_matches(slice(2, 3), 1) |
|
4264 False |
|
4265 """ |
|
4266 if isinstance(level, slice): |
|
4267 start, stop = level.start, level.stop |
|
4268 if start is not None and start > consumer_level: |
|
4269 return False |
|
4270 if stop is not None or stop <= consumer_level: |
|
4271 return False |
|
4272 return True |
|
4273 else: |
|
4274 return level >= consumer_level |
|
4275 |
|
4276 @classmethod |
|
4277 def level_for_integer(cls, level): |
|
4278 levels = cls.LEVELS |
|
4279 if level < 0: |
|
4280 return levels[0] |
|
4281 if level >= len(levels): |
|
4282 return levels[-1] |
|
4283 return levels[level] |
|
4284 |
|
4285 def move_stdout_to_stderr(self): |
|
4286 to_remove = [] |
|
4287 to_add = [] |
|
4288 for consumer_level, consumer in self.consumers: |
|
4289 if consumer == sys.stdout: |
|
4290 to_remove.append((consumer_level, consumer)) |
|
4291 to_add.append((consumer_level, sys.stderr)) |
|
4292 for item in to_remove: |
|
4293 self.consumers.remove(item) |
|
4294 self.consumers.extend(to_add) |
|
4295 |
|
4296 |
|
4297 def call_subprocess(cmd, show_stdout=True, |
|
4298 filter_stdout=None, cwd=None, |
|
4299 raise_on_returncode=True, |
|
4300 command_level=Logger.DEBUG, command_desc=None, |
|
4301 extra_environ=None): |
|
4302 if command_desc is None: |
|
4303 cmd_parts = [] |
|
4304 for part in cmd: |
|
4305 if ' ' in part or '\n' in part or '"' in part or "'" in part: |
|
4306 part = '"%s"' % part.replace('"', '\\"') |
|
4307 cmd_parts.append(part) |
|
4308 command_desc = ' '.join(cmd_parts) |
|
4309 if show_stdout: |
|
4310 stdout = None |
|
4311 else: |
|
4312 stdout = subprocess.PIPE |
|
4313 logger.log(command_level, "Running command %s" % command_desc) |
|
4314 env = os.environ.copy() |
|
4315 if extra_environ: |
|
4316 env.update(extra_environ) |
|
4317 try: |
|
4318 proc = subprocess.Popen( |
|
4319 cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout, |
|
4320 cwd=cwd, env=env) |
|
4321 except Exception, e: |
|
4322 logger.fatal( |
|
4323 "Error %s while executing command %s" % (e, command_desc)) |
|
4324 raise |
|
4325 all_output = [] |
|
4326 if stdout is not None: |
|
4327 stdout = proc.stdout |
|
4328 while 1: |
|
4329 line = stdout.readline() |
|
4330 if not line: |
|
4331 break |
|
4332 line = line.rstrip() |
|
4333 all_output.append(line + '\n') |
|
4334 if filter_stdout: |
|
4335 level = filter_stdout(line) |
|
4336 if isinstance(level, tuple): |
|
4337 level, line = level |
|
4338 logger.log(level, line) |
|
4339 if not logger.stdout_level_matches(level): |
|
4340 logger.show_progress() |
|
4341 else: |
|
4342 logger.info(line) |
|
4343 else: |
|
4344 returned_stdout, returned_stderr = proc.communicate() |
|
4345 all_output = [returned_stdout or ''] |
|
4346 proc.wait() |
|
4347 if proc.returncode: |
|
4348 if raise_on_returncode: |
|
4349 if all_output: |
|
4350 logger.notify('Complete output from command %s:' % command_desc) |
|
4351 logger.notify('\n'.join(all_output) + '\n----------------------------------------') |
|
4352 raise InstallationError( |
|
4353 "Command %s failed with error code %s" |
|
4354 % (command_desc, proc.returncode)) |
|
4355 else: |
|
4356 logger.warn( |
|
4357 "Command %s had error code %s" |
|
4358 % (command_desc, proc.returncode)) |
|
4359 if stdout is not None: |
|
4360 return ''.join(all_output) |
|
4361 |
|
4362 ############################################################ |
|
4363 ## Utility functions |
|
4364 |
|
4365 def is_svn_page(html): |
|
4366 """Returns true if the page appears to be the index page of an svn repository""" |
|
4367 return (re.search(r'<title>[^<]*Revision \d+:', html) |
|
4368 and re.search(r'Powered by (?:<a[^>]*?>)?Subversion', html, re.I)) |
|
4369 |
|
4370 def file_contents(filename): |
|
4371 fp = open(filename, 'rb') |
|
4372 try: |
|
4373 return fp.read() |
|
4374 finally: |
|
4375 fp.close() |
|
4376 |
|
4377 def split_leading_dir(path): |
|
4378 path = str(path) |
|
4379 path = path.lstrip('/').lstrip('\\') |
|
4380 if '/' in path and (('\\' in path and path.find('/') < path.find('\\')) |
|
4381 or '\\' not in path): |
|
4382 return path.split('/', 1) |
|
4383 elif '\\' in path: |
|
4384 return path.split('\\', 1) |
|
4385 else: |
|
4386 return path, '' |
|
4387 |
|
4388 def has_leading_dir(paths): |
|
4389 """Returns true if all the paths have the same leading path name |
|
4390 (i.e., everything is in one subdirectory in an archive)""" |
|
4391 common_prefix = None |
|
4392 for path in paths: |
|
4393 prefix, rest = split_leading_dir(path) |
|
4394 if not prefix: |
|
4395 return False |
|
4396 elif common_prefix is None: |
|
4397 common_prefix = prefix |
|
4398 elif prefix != common_prefix: |
|
4399 return False |
|
4400 return True |
|
4401 |
|
4402 def format_size(bytes): |
|
4403 if bytes > 1000*1000: |
|
4404 return '%.1fMb' % (bytes/1000.0/1000) |
|
4405 elif bytes > 10*1000: |
|
4406 return '%iKb' % (bytes/1000) |
|
4407 elif bytes > 1000: |
|
4408 return '%.1fKb' % (bytes/1000.0) |
|
4409 else: |
|
4410 return '%ibytes' % bytes |
|
4411 |
|
4412 _normalize_re = re.compile(r'[^a-z]', re.I) |
|
4413 |
|
4414 def normalize_name(name): |
|
4415 return _normalize_re.sub('-', name.lower()) |
|
4416 |
|
4417 def make_path_relative(path, rel_to): |
|
4418 """ |
|
4419 Make a filename relative, where the filename path, and it is |
|
4420 relative to rel_to |
|
4421 |
|
4422 >>> make_relative_path('/usr/share/something/a-file.pth', |
|
4423 ... '/usr/share/another-place/src/Directory') |
|
4424 '../../../something/a-file.pth' |
|
4425 >>> make_relative_path('/usr/share/something/a-file.pth', |
|
4426 ... '/home/user/src/Directory') |
|
4427 '../../../usr/share/something/a-file.pth' |
|
4428 >>> make_relative_path('/usr/share/a-file.pth', '/usr/share/') |
|
4429 'a-file.pth' |
|
4430 """ |
|
4431 path_filename = os.path.basename(path) |
|
4432 path = os.path.dirname(path) |
|
4433 path = os.path.normpath(os.path.abspath(path)) |
|
4434 rel_to = os.path.normpath(os.path.abspath(rel_to)) |
|
4435 path_parts = path.strip(os.path.sep).split(os.path.sep) |
|
4436 rel_to_parts = rel_to.strip(os.path.sep).split(os.path.sep) |
|
4437 while path_parts and rel_to_parts and path_parts[0] == rel_to_parts[0]: |
|
4438 path_parts.pop(0) |
|
4439 rel_to_parts.pop(0) |
|
4440 full_parts = ['..']*len(rel_to_parts) + path_parts + [path_filename] |
|
4441 if full_parts == ['']: |
|
4442 return '.' + os.path.sep |
|
4443 return os.path.sep.join(full_parts) |
|
4444 |
|
4445 def display_path(path): |
|
4446 """Gives the display value for a given path, making it relative to cwd |
|
4447 if possible.""" |
|
4448 path = os.path.normcase(os.path.abspath(path)) |
|
4449 if path.startswith(os.getcwd() + os.path.sep): |
|
4450 path = '.' + path[len(os.getcwd()):] |
|
4451 return path |
|
4452 |
|
4453 def parse_editable(editable_req, default_vcs=None): |
|
4454 """Parses svn+http://blahblah@rev#egg=Foobar into a requirement |
|
4455 (Foobar) and a URL""" |
|
4456 url = editable_req |
|
4457 if os.path.isdir(url) and os.path.exists(os.path.join(url, 'setup.py')): |
|
4458 # Treating it as code that has already been checked out |
|
4459 url = filename_to_url(url) |
|
4460 if url.lower().startswith('file:'): |
|
4461 return None, url |
|
4462 for version_control in vcs: |
|
4463 if url.lower().startswith('%s:' % version_control): |
|
4464 url = '%s+%s' % (version_control, url) |
|
4465 if '+' not in url: |
|
4466 if default_vcs: |
|
4467 url = default_vcs + '+' + url |
|
4468 else: |
|
4469 raise InstallationError( |
|
4470 '--editable=%s should be formatted with svn+URL, git+URL, hg+URL or bzr+URL' % editable_req) |
|
4471 vc_type = url.split('+', 1)[0].lower() |
|
4472 if not vcs.get_backend(vc_type): |
|
4473 raise InstallationError( |
|
4474 'For --editable=%s only svn (svn+URL), Git (git+URL), Mercurial (hg+URL) and Bazaar (bzr+URL) is currently supported' % editable_req) |
|
4475 match = re.search(r'(?:#|#.*?&)egg=([^&]*)', editable_req) |
|
4476 if (not match or not match.group(1)) and vcs.get_backend(vc_type): |
|
4477 parts = [p for p in editable_req.split('#', 1)[0].split('/') if p] |
|
4478 if parts[-2] in ('tags', 'branches', 'tag', 'branch'): |
|
4479 req = parts[-3] |
|
4480 elif parts[-1] == 'trunk': |
|
4481 req = parts[-2] |
|
4482 else: |
|
4483 raise InstallationError( |
|
4484 '--editable=%s is not the right format; it must have #egg=Package' |
|
4485 % editable_req) |
|
4486 else: |
|
4487 req = match.group(1) |
|
4488 ## FIXME: use package_to_requirement? |
|
4489 match = re.search(r'^(.*?)(?:-dev|-\d.*)', req) |
|
4490 if match: |
|
4491 # Strip off -dev, -0.2, etc. |
|
4492 req = match.group(1) |
|
4493 return req, url |
|
4494 |
|
4495 def backup_dir(dir, ext='.bak'): |
|
4496 """Figure out the name of a directory to back up the given dir to |
|
4497 (adding .bak, .bak2, etc)""" |
|
4498 n = 1 |
|
4499 extension = ext |
|
4500 while os.path.exists(dir + extension): |
|
4501 n += 1 |
|
4502 extension = ext + str(n) |
|
4503 return dir + extension |
|
4504 |
|
4505 def ask(message, options): |
|
4506 """Ask the message interactively, with the given possible responses""" |
|
4507 while 1: |
|
4508 if os.environ.get('PIP_NO_INPUT'): |
|
4509 raise Exception('No input was expected ($PIP_NO_INPUT set); question: %s' % message) |
|
4510 response = raw_input(message) |
|
4511 response = response.strip().lower() |
|
4512 if response not in options: |
|
4513 print 'Your response (%r) was not one of the expected responses: %s' % ( |
|
4514 response, ', '.join(options)) |
|
4515 else: |
|
4516 return response |
|
4517 |
|
4518 def open_logfile_append(filename): |
|
4519 """Open the named log file in append mode. |
|
4520 |
|
4521 If the file already exists, a separator will also be printed to |
|
4522 the file to separate past activity from current activity. |
|
4523 """ |
|
4524 exists = os.path.exists(filename) |
|
4525 log_fp = open(filename, 'a') |
|
4526 if exists: |
|
4527 print >> log_fp, '-'*60 |
|
4528 print >> log_fp, '%s run on %s' % (sys.argv[0], time.strftime('%c')) |
|
4529 return log_fp |
|
4530 |
|
4531 def is_url(name): |
|
4532 """Returns true if the name looks like a URL""" |
|
4533 if ':' not in name: |
|
4534 return False |
|
4535 scheme = name.split(':', 1)[0].lower() |
|
4536 return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes |
|
4537 |
|
4538 def is_filename(name): |
|
4539 if (splitext(name)[1].lower() in ('.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar', '.pybundle') |
|
4540 and os.path.exists(name)): |
|
4541 return True |
|
4542 if os.path.sep not in name and '/' not in name: |
|
4543 # Doesn't have any path components, probably a requirement like 'Foo' |
|
4544 return False |
|
4545 return True |
|
4546 |
|
4547 _drive_re = re.compile('^([a-z]):', re.I) |
|
4548 _url_drive_re = re.compile('^([a-z])[:|]', re.I) |
|
4549 |
|
4550 def filename_to_url(filename): |
|
4551 """ |
|
4552 Convert a path to a file: URL. The path will be made absolute. |
|
4553 """ |
|
4554 filename = os.path.normcase(os.path.abspath(filename)) |
|
4555 if _drive_re.match(filename): |
|
4556 filename = filename[0] + '|' + filename[2:] |
|
4557 url = urllib.quote(filename) |
|
4558 url = url.replace(os.path.sep, '/') |
|
4559 url = url.lstrip('/') |
|
4560 return 'file:///' + url |
|
4561 |
|
4562 def filename_to_url2(filename): |
|
4563 """ |
|
4564 Convert a path to a file: URL. The path will be made absolute and have |
|
4565 quoted path parts. |
|
4566 """ |
|
4567 filename = os.path.normcase(os.path.abspath(filename)) |
|
4568 drive, filename = os.path.splitdrive(filename) |
|
4569 filepath = filename.split(os.path.sep) |
|
4570 url = '/'.join([urllib.quote(part) for part in filepath]) |
|
4571 if not drive: |
|
4572 url = url.lstrip('/') |
|
4573 return 'file:///' + drive + url |
|
4574 |
|
4575 def url_to_filename(url): |
|
4576 """ |
|
4577 Convert a file: URL to a path. |
|
4578 """ |
|
4579 assert url.startswith('file:'), ( |
|
4580 "You can only turn file: urls into filenames (not %r)" % url) |
|
4581 filename = url[len('file:'):].lstrip('/') |
|
4582 filename = urllib.unquote(filename) |
|
4583 if _url_drive_re.match(filename): |
|
4584 filename = filename[0] + ':' + filename[2:] |
|
4585 else: |
|
4586 filename = '/' + filename |
|
4587 return filename |
|
4588 |
|
4589 def get_requirement_from_url(url): |
|
4590 """Get a requirement from the URL, if possible. This looks for #egg |
|
4591 in the URL""" |
|
4592 link = Link(url) |
|
4593 egg_info = link.egg_fragment |
|
4594 if not egg_info: |
|
4595 egg_info = splitext(link.filename)[0] |
|
4596 return package_to_requirement(egg_info) |
|
4597 |
|
4598 def package_to_requirement(package_name): |
|
4599 """Translate a name like Foo-1.2 to Foo==1.3""" |
|
4600 match = re.search(r'^(.*?)(-dev|-\d.*)', package_name) |
|
4601 if match: |
|
4602 name = match.group(1) |
|
4603 version = match.group(2) |
|
4604 else: |
|
4605 name = package_name |
|
4606 version = '' |
|
4607 if version: |
|
4608 return '%s==%s' % (name, version) |
|
4609 else: |
|
4610 return name |
|
4611 |
|
4612 def is_framework_layout(path): |
|
4613 """Return True if the current platform is the default Python of Mac OS X |
|
4614 which installs scripts in /usr/local/bin""" |
|
4615 return (sys.platform[:6] == 'darwin' and |
|
4616 (path[:9] == '/Library/' or path[:16] == '/System/Library/')) |
|
4617 |
|
4618 def strip_prefix(path, prefix): |
|
4619 """ If ``path`` begins with ``prefix``, return ``path`` with |
|
4620 ``prefix`` stripped off. Otherwise return None.""" |
|
4621 prefixes = [prefix] |
|
4622 # Yep, we are special casing the framework layout of MacPython here |
|
4623 if is_framework_layout(sys.prefix): |
|
4624 for location in ('/Library', '/usr/local'): |
|
4625 if path.startswith(location): |
|
4626 prefixes.append(location) |
|
4627 for prefix in prefixes: |
|
4628 if path.startswith(prefix): |
|
4629 return prefix, path.replace(prefix + os.path.sep, '') |
|
4630 return None, None |
|
4631 |
|
4632 class UninstallPathSet(object): |
|
4633 """A set of file paths to be removed in the uninstallation of a |
|
4634 requirement.""" |
|
4635 def __init__(self, dist, restrict_to_prefix): |
|
4636 self.paths = set() |
|
4637 self._refuse = set() |
|
4638 self.pth = {} |
|
4639 self.prefix = os.path.normcase(os.path.realpath(restrict_to_prefix)) |
|
4640 self.dist = dist |
|
4641 self.location = dist.location |
|
4642 self.save_dir = None |
|
4643 self._moved_paths = [] |
|
4644 |
|
4645 def _can_uninstall(self): |
|
4646 prefix, stripped = strip_prefix(self.location, self.prefix) |
|
4647 if not stripped: |
|
4648 logger.notify("Not uninstalling %s at %s, outside environment %s" |
|
4649 % (self.dist.project_name, self.dist.location, |
|
4650 self.prefix)) |
|
4651 return False |
|
4652 return True |
|
4653 |
|
4654 def add(self, path): |
|
4655 path = os.path.abspath(path) |
|
4656 if not os.path.exists(path): |
|
4657 return |
|
4658 prefix, stripped = strip_prefix(os.path.normcase(path), self.prefix) |
|
4659 if stripped: |
|
4660 self.paths.add((prefix, stripped)) |
|
4661 else: |
|
4662 self._refuse.add((prefix, path)) |
|
4663 |
|
4664 def add_pth(self, pth_file, entry): |
|
4665 prefix, stripped = strip_prefix(os.path.normcase(pth_file), self.prefix) |
|
4666 if stripped: |
|
4667 entry = os.path.normcase(entry) |
|
4668 if stripped not in self.pth: |
|
4669 self.pth[stripped] = UninstallPthEntries(os.path.join(prefix, stripped)) |
|
4670 self.pth[stripped].add(os.path.normcase(entry)) |
|
4671 else: |
|
4672 self._refuse.add((prefix, pth_file)) |
|
4673 |
|
4674 def compact(self, paths): |
|
4675 """Compact a path set to contain the minimal number of paths |
|
4676 necessary to contain all paths in the set. If /a/path/ and |
|
4677 /a/path/to/a/file.txt are both in the set, leave only the |
|
4678 shorter path.""" |
|
4679 short_paths = set() |
|
4680 def sort_set(x, y): |
|
4681 prefix_x, path_x = x |
|
4682 prefix_y, path_y = y |
|
4683 return cmp(len(path_x), len(path_y)) |
|
4684 for prefix, path in sorted(paths, sort_set): |
|
4685 if not any([(path.startswith(shortpath) and |
|
4686 path[len(shortpath.rstrip(os.path.sep))] == os.path.sep) |
|
4687 for shortprefix, shortpath in short_paths]): |
|
4688 short_paths.add((prefix, path)) |
|
4689 return short_paths |
|
4690 |
|
4691 def remove(self, auto_confirm=False): |
|
4692 """Remove paths in ``self.paths`` with confirmation (unless |
|
4693 ``auto_confirm`` is True).""" |
|
4694 if not self._can_uninstall(): |
|
4695 return |
|
4696 logger.notify('Uninstalling %s:' % self.dist.project_name) |
|
4697 logger.indent += 2 |
|
4698 paths = sorted(self.compact(self.paths)) |
|
4699 try: |
|
4700 if auto_confirm: |
|
4701 response = 'y' |
|
4702 else: |
|
4703 for prefix, path in paths: |
|
4704 logger.notify(os.path.join(prefix, path)) |
|
4705 response = ask('Proceed (y/n)? ', ('y', 'n')) |
|
4706 if self._refuse: |
|
4707 logger.notify('Not removing or modifying (outside of prefix):') |
|
4708 for prefix, path in self.compact(self._refuse): |
|
4709 logger.notify(os.path.join(prefix, path)) |
|
4710 if response == 'y': |
|
4711 self.save_dir = tempfile.mkdtemp('-uninstall', 'pip-') |
|
4712 for prefix, path in paths: |
|
4713 full_path = os.path.join(prefix, path) |
|
4714 new_path = os.path.join(self.save_dir, path) |
|
4715 new_dir = os.path.dirname(new_path) |
|
4716 logger.info('Removing file or directory %s' % full_path) |
|
4717 self._moved_paths.append((prefix, path)) |
|
4718 os.renames(full_path, new_path) |
|
4719 for pth in self.pth.values(): |
|
4720 pth.remove() |
|
4721 logger.notify('Successfully uninstalled %s' % self.dist.project_name) |
|
4722 |
|
4723 finally: |
|
4724 logger.indent -= 2 |
|
4725 |
|
4726 def rollback(self): |
|
4727 """Rollback the changes previously made by remove().""" |
|
4728 if self.save_dir is None: |
|
4729 logger.error("Can't roll back %s; was not uninstalled" % self.dist.project_name) |
|
4730 return False |
|
4731 logger.notify('Rolling back uninstall of %s' % self.dist.project_name) |
|
4732 for prefix, path in self._moved_paths: |
|
4733 tmp_path = os.path.join(self.save_dir, path) |
|
4734 real_path = os.path.join(prefix, path) |
|
4735 logger.info('Replacing %s' % real_path) |
|
4736 os.renames(tmp_path, real_path) |
|
4737 for pth in self.pth: |
|
4738 pth.rollback() |
|
4739 |
|
4740 def commit(self): |
|
4741 """Remove temporary save dir: rollback will no longer be possible.""" |
|
4742 if self.save_dir is not None: |
|
4743 shutil.rmtree(self.save_dir) |
|
4744 self.save_dir = None |
|
4745 self._moved_paths = [] |
|
4746 |
|
4747 |
|
4748 class UninstallPthEntries(object): |
|
4749 def __init__(self, pth_file): |
|
4750 if not os.path.isfile(pth_file): |
|
4751 raise UninstallationError("Cannot remove entries from nonexistent file %s" % pth_file) |
|
4752 self.file = pth_file |
|
4753 self.entries = set() |
|
4754 self._saved_lines = None |
|
4755 |
|
4756 def add(self, entry): |
|
4757 self.entries.add(entry) |
|
4758 |
|
4759 def remove(self): |
|
4760 logger.info('Removing pth entries from %s:' % self.file) |
|
4761 fh = open(self.file, 'r') |
|
4762 lines = fh.readlines() |
|
4763 self._saved_lines = lines |
|
4764 fh.close() |
|
4765 try: |
|
4766 for entry in self.entries: |
|
4767 logger.info('Removing entry: %s' % entry) |
|
4768 try: |
|
4769 lines.remove(entry + '\n') |
|
4770 except ValueError: |
|
4771 pass |
|
4772 finally: |
|
4773 pass |
|
4774 fh = open(self.file, 'w') |
|
4775 fh.writelines(lines) |
|
4776 fh.close() |
|
4777 |
|
4778 def rollback(self): |
|
4779 if self._saved_lines is None: |
|
4780 logger.error('Cannot roll back changes to %s, none were made' % self.file) |
|
4781 return False |
|
4782 logger.info('Rolling %s back to previous state' % self.file) |
|
4783 fh = open(self.file, 'w') |
|
4784 fh.writelines(self._saved_lines) |
|
4785 fh.close() |
|
4786 return True |
|
4787 |
|
4788 class FakeFile(object): |
|
4789 """Wrap a list of lines in an object with readline() to make |
|
4790 ConfigParser happy.""" |
|
4791 def __init__(self, lines): |
|
4792 self._gen = (l for l in lines) |
|
4793 |
|
4794 def readline(self): |
|
4795 try: |
|
4796 return self._gen.next() |
|
4797 except StopIteration: |
|
4798 return '' |
|
4799 |
|
4800 def splitext(path): |
|
4801 """Like os.path.splitext, but take off .tar too""" |
|
4802 base, ext = posixpath.splitext(path) |
|
4803 if base.lower().endswith('.tar'): |
|
4804 ext = base[-4:] + ext |
|
4805 base = base[:-4] |
|
4806 return base, ext |
|
4807 |
|
4808 def find_command(cmd, paths=None, pathext=None): |
|
4809 """Searches the PATH for the given command and returns its path""" |
|
4810 if paths is None: |
|
4811 paths = os.environ.get('PATH', []).split(os.pathsep) |
|
4812 if isinstance(paths, basestring): |
|
4813 paths = [paths] |
|
4814 # check if there are funny path extensions for executables, e.g. Windows |
|
4815 if pathext is None: |
|
4816 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD') |
|
4817 pathext = [ext for ext in pathext.lower().split(os.pathsep)] |
|
4818 # don't use extensions if the command ends with one of them |
|
4819 if os.path.splitext(cmd)[1].lower() in pathext: |
|
4820 pathext = [''] |
|
4821 # check if we find the command on PATH |
|
4822 for path in paths: |
|
4823 # try without extension first |
|
4824 cmd_path = os.path.join(path, cmd) |
|
4825 for ext in pathext: |
|
4826 # then including the extension |
|
4827 cmd_path_ext = cmd_path + ext |
|
4828 if os.path.exists(cmd_path_ext): |
|
4829 return cmd_path_ext |
|
4830 if os.path.exists(cmd_path): |
|
4831 return cmd_path |
|
4832 return None |
|
4833 |
|
4834 class _Inf(object): |
|
4835 """I am bigger than everything!""" |
|
4836 def __cmp__(self, a): |
|
4837 if self is a: |
|
4838 return 0 |
|
4839 return 1 |
|
4840 def __repr__(self): |
|
4841 return 'Inf' |
|
4842 Inf = _Inf() |
|
4843 del _Inf |
|
4844 |
|
4845 if __name__ == '__main__': |
|
4846 exit = main() |
|
4847 if exit: |
|
4848 sys.exit(exit) |