1 """Package Install Manager for Python. 2 3 This is currently a MacOSX-only strawman implementation. 4 Despite other rumours the name stands for "Packman IMPlementation". 5 6 Tools to allow easy installation of packages. The idea is that there is 7 an online XML database per (platform, python-version) containing packages 8 known to work with that combination. This module contains tools for getting 9 and parsing the database, testing whether packages are installed, computing 10 dependencies and installing packages. 11 12 There is a minimal main program that works as a command line tool, but the 13 intention is that the end user will use this through a GUI. 14 """ 15 16 from warnings import warnpy3k 17 warnpy3k("In 3.x, the pimp module is removed.", stacklevel=2) 18 19 import sys 20 import os 21 import subprocess 22 import urllib 23 import urllib2 24 import urlparse 25 import plistlib 26 import distutils.util 27 import distutils.sysconfig 28 import hashlib 29 import tarfile 30 import tempfile 31 import shutil 32 import time 33 34 __all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main", 35 "getDefaultDatabase", "PIMP_VERSION", "main"] 36 37 _scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled" 38 _scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled" 39 _scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled" 40 41 NO_EXECUTE=0 42 43 PIMP_VERSION="0.5" 44 45 # Flavors: 46 # source: setup-based package 47 # binary: tar (or other) archive created with setup.py bdist. 48 # installer: something that can be opened 49 DEFAULT_FLAVORORDER=['source', 'binary', 'installer'] 50 DEFAULT_DOWNLOADDIR='/tmp' 51 DEFAULT_BUILDDIR='/tmp' 52 DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib() 53 DEFAULT_PIMPDATABASE_FMT="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist" 54 55 def getDefaultDatabase(experimental=False): 56 if experimental: 57 status = "exp" 58 else: 59 status = "prod" 60 61 major, minor, micro, state, extra = sys.version_info 62 pyvers = '%d.%d' % (major, minor) 63 if micro == 0 and state != 'final': 64 pyvers = pyvers + '%s%d' % (state, extra) 65 66 longplatform = distutils.util.get_platform() 67 osname, release, machine = longplatform.split('-') 68 # For some platforms we may want to differentiate between 69 # installation types 70 if osname == 'darwin': 71 if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'): 72 osname = 'darwin_apple' 73 elif sys.prefix.startswith('/Library/Frameworks/Python.framework'): 74 osname = 'darwin_macpython' 75 # Otherwise we don't know... 76 # Now we try various URLs by playing with the release string. 77 # We remove numbers off the end until we find a match. 78 rel = release 79 while True: 80 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine) 81 try: 82 urllib2.urlopen(url) 83 except urllib2.HTTPError, arg: 84 pass 85 else: 86 break 87 if not rel: 88 # We're out of version numbers to try. Use the 89 # full release number, this will give a reasonable 90 # error message later 91 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine) 92 break 93 idx = rel.rfind('.') 94 if idx < 0: 95 rel = '' 96 else: 97 rel = rel[:idx] 98 return url 99 100 def _cmd(output, dir, *cmditems): 101 """Internal routine to run a shell command in a given directory.""" 102 103 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems) 104 if output: 105 output.write("+ %s\n" % cmd) 106 if NO_EXECUTE: 107 return 0 108 child = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, 109 stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 110 child.stdin.close() 111 while 1: 112 line = child.stdout.readline() 113 if not line: 114 break 115 if output: 116 output.write(line) 117 return child.wait() 118 119 class PimpDownloader: 120 """Abstract base class - Downloader for archives""" 121 122 def __init__(self, argument, 123 dir="", 124 watcher=None): 125 self.argument = argument 126 self._dir = dir 127 self._watcher = watcher 128 129 def download(self, url, filename, output=None): 130 return None 131 132 def update(self, str): 133 if self._watcher: 134 return self._watcher.update(str) 135 return True 136 137 class PimpCurlDownloader(PimpDownloader): 138 139 def download(self, url, filename, output=None): 140 self.update("Downloading %s..." % url) 141 exitstatus = _cmd(output, self._dir, 142 "curl", 143 "--output", filename, 144 url) 145 self.update("Downloading %s: finished" % url) 146 return (not exitstatus) 147 148 class PimpUrllibDownloader(PimpDownloader): 149 150 def download(self, url, filename, output=None): 151 output = open(filename, 'wb') 152 self.update("Downloading %s: opening connection" % url) 153 keepgoing = True 154 download = urllib2.urlopen(url) 155 if 'content-length' in download.headers: 156 length = long(download.headers['content-length']) 157 else: 158 length = -1 159 160 data = download.read(4096) #read 4K at a time 161 dlsize = 0 162 lasttime = 0 163 while keepgoing: 164 dlsize = dlsize + len(data) 165 if len(data) == 0: 166 #this is our exit condition 167 break 168 output.write(data) 169 if int(time.time()) != lasttime: 170 # Update at most once per second 171 lasttime = int(time.time()) 172 if length == -1: 173 keepgoing = self.update("Downloading %s: %d bytes..." % (url, dlsize)) 174 else: 175 keepgoing = self.update("Downloading %s: %d%% (%d bytes)..." % (url, int(100.0*dlsize/length), dlsize)) 176 data = download.read(4096) 177 if keepgoing: 178 self.update("Downloading %s: finished" % url) 179 return keepgoing 180 181 class PimpUnpacker: 182 """Abstract base class - Unpacker for archives""" 183 184 _can_rename = False 185 186 def __init__(self, argument, 187 dir="", 188 renames=[], 189 watcher=None): 190 self.argument = argument 191 if renames and not self._can_rename: 192 raise RuntimeError, "This unpacker cannot rename files" 193 self._dir = dir 194 self._renames = renames 195 self._watcher = watcher 196 197 def unpack(self, archive, output=None, package=None): 198 return None 199 200 def update(self, str): 201 if self._watcher: 202 return self._watcher.update(str) 203 return True 204 205 class PimpCommandUnpacker(PimpUnpacker): 206 """Unpack archives by calling a Unix utility""" 207 208 _can_rename = False 209 210 def unpack(self, archive, output=None, package=None): 211 cmd = self.argument % archive 212 if _cmd(output, self._dir, cmd): 213 return "unpack command failed" 214 215 class PimpTarUnpacker(PimpUnpacker): 216 """Unpack tarfiles using the builtin tarfile module""" 217 218 _can_rename = True 219 220 def unpack(self, archive, output=None, package=None): 221 tf = tarfile.open(archive, "r") 222 members = tf.getmembers() 223 skip = [] 224 if self._renames: 225 for member in members: 226 for oldprefix, newprefix in self._renames: 227 if oldprefix[:len(self._dir)] == self._dir: 228 oldprefix2 = oldprefix[len(self._dir):] 229 else: 230 oldprefix2 = None 231 if member.name[:len(oldprefix)] == oldprefix: 232 if newprefix is None: 233 skip.append(member) 234 #print 'SKIP', member.name 235 else: 236 member.name = newprefix + member.name[len(oldprefix):] 237 print ' ', member.name 238 break 239 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2: 240 if newprefix is None: 241 skip.append(member) 242 #print 'SKIP', member.name 243 else: 244 member.name = newprefix + member.name[len(oldprefix2):] 245 #print ' ', member.name 246 break 247 else: 248 skip.append(member) 249 #print '????', member.name 250 for member in members: 251 if member in skip: 252 self.update("Skipping %s" % member.name) 253 continue 254 self.update("Extracting %s" % member.name) 255 tf.extract(member, self._dir) 256 if skip: 257 names = [member.name for member in skip if member.name[-1] != '/'] 258 if package: 259 names = package.filterExpectedSkips(names) 260 if names: 261 return "Not all files were unpacked: %s" % " ".join(names) 262 263 ARCHIVE_FORMATS = [ 264 (".tar.Z", PimpTarUnpacker, None), 265 (".taz", PimpTarUnpacker, None), 266 (".tar.gz", PimpTarUnpacker, None), 267 (".tgz", PimpTarUnpacker, None), 268 (".tar.bz", PimpTarUnpacker, None), 269 (".zip", PimpCommandUnpacker, "unzip \"%s\""), 270 ] 271 272 class PimpPreferences: 273 """Container for per-user preferences, such as the database to use 274 and where to install packages.""" 275 276 def __init__(self, 277 flavorOrder=None, 278 downloadDir=None, 279 buildDir=None, 280 installDir=None, 281 pimpDatabase=None): 282 if not flavorOrder: 283 flavorOrder = DEFAULT_FLAVORORDER 284 if not downloadDir: 285 downloadDir = DEFAULT_DOWNLOADDIR 286 if not buildDir: 287 buildDir = DEFAULT_BUILDDIR 288 if not pimpDatabase: 289 pimpDatabase = getDefaultDatabase() 290 self.setInstallDir(installDir) 291 self.flavorOrder = flavorOrder 292 self.downloadDir = downloadDir 293 self.buildDir = buildDir 294 self.pimpDatabase = pimpDatabase 295 self.watcher = None 296 297 def setWatcher(self, watcher): 298 self.watcher = watcher 299 300 def setInstallDir(self, installDir=None): 301 if installDir: 302 # Installing to non-standard location. 303 self.installLocations = [ 304 ('--install-lib', installDir), 305 ('--install-headers', None), 306 ('--install-scripts', None), 307 ('--install-data', None)] 308 else: 309 installDir = DEFAULT_INSTALLDIR 310 self.installLocations = [] 311 self.installDir = installDir 312 313 def isUserInstall(self): 314 return self.installDir != DEFAULT_INSTALLDIR 315 316 def check(self): 317 """Check that the preferences make sense: directories exist and are 318 writable, the install directory is on sys.path, etc.""" 319 320 rv = "" 321 RWX_OK = os.R_OK|os.W_OK|os.X_OK 322 if not os.path.exists(self.downloadDir): 323 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir 324 elif not os.access(self.downloadDir, RWX_OK): 325 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir 326 if not os.path.exists(self.buildDir): 327 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir 328 elif not os.access(self.buildDir, RWX_OK): 329 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir 330 if not os.path.exists(self.installDir): 331 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir 332 elif not os.access(self.installDir, RWX_OK): 333 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir 334 else: 335 installDir = os.path.realpath(self.installDir) 336 for p in sys.path: 337 try: 338 realpath = os.path.realpath(p) 339 except: 340 pass 341 if installDir == realpath: 342 break 343 else: 344 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir 345 return rv 346 347 def compareFlavors(self, left, right): 348 """Compare two flavor strings. This is part of your preferences 349 because whether the user prefers installing from source or binary is.""" 350 if left in self.flavorOrder: 351 if right in self.flavorOrder: 352 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right)) 353 return -1 354 if right in self.flavorOrder: 355 return 1 356 return cmp(left, right) 357 358 class PimpDatabase: 359 """Class representing a pimp database. It can actually contain 360 information from multiple databases through inclusion, but the 361 toplevel database is considered the master, as its maintainer is 362 "responsible" for the contents.""" 363 364 def __init__(self, prefs): 365 self._packages = [] 366 self.preferences = prefs 367 self._url = "" 368 self._urllist = [] 369 self._version = "" 370 self._maintainer = "" 371 self._description = "" 372 373 # Accessor functions 374 def url(self): return self._url 375 def version(self): return self._version 376 def maintainer(self): return self._maintainer 377 def description(self): return self._description 378 379 def close(self): 380 """Clean up""" 381 self._packages = [] 382 self.preferences = None 383 384 def appendURL(self, url, included=0): 385 """Append packages from the database with the given URL. 386 Only the first database should specify included=0, so the 387 global information (maintainer, description) get stored.""" 388 389 if url in self._urllist: 390 return 391 self._urllist.append(url) 392 fp = urllib2.urlopen(url).fp 393 plistdata = plistlib.Plist.fromFile(fp) 394 # Test here for Pimp version, etc 395 if included: 396 version = plistdata.get('Version') 397 if version and version > self._version: 398 sys.stderr.write("Warning: included database %s is for pimp version %s\n" % 399 (url, version)) 400 else: 401 self._version = plistdata.get('Version') 402 if not self._version: 403 sys.stderr.write("Warning: database has no Version information\n") 404 elif self._version > PIMP_VERSION: 405 sys.stderr.write("Warning: database version %s newer than pimp version %s\n" 406 % (self._version, PIMP_VERSION)) 407 self._maintainer = plistdata.get('Maintainer', '') 408 self._description = plistdata.get('Description', '').strip() 409 self._url = url 410 self._appendPackages(plistdata['Packages'], url) 411 others = plistdata.get('Include', []) 412 for o in others: 413 o = urllib.basejoin(url, o) 414 self.appendURL(o, included=1) 415 416 def _appendPackages(self, packages, url): 417 """Given a list of dictionaries containing package 418 descriptions create the PimpPackage objects and append them 419 to our internal storage.""" 420 421 for p in packages: 422 p = dict(p) 423 if 'Download-URL' in p: 424 p['Download-URL'] = urllib.basejoin(url, p['Download-URL']) 425 flavor = p.get('Flavor') 426 if flavor == 'source': 427 pkg = PimpPackage_source(self, p) 428 elif flavor == 'binary': 429 pkg = PimpPackage_binary(self, p) 430 elif flavor == 'installer': 431 pkg = PimpPackage_installer(self, p) 432 elif flavor == 'hidden': 433 pkg = PimpPackage_installer(self, p) 434 else: 435 pkg = PimpPackage(self, dict(p)) 436 self._packages.append(pkg) 437 438 def list(self): 439 """Return a list of all PimpPackage objects in the database.""" 440 441 return self._packages 442 443 def listnames(self): 444 """Return a list of names of all packages in the database.""" 445 446 rv = [] 447 for pkg in self._packages: 448 rv.append(pkg.fullname()) 449 rv.sort() 450 return rv 451 452 def dump(self, pathOrFile): 453 """Dump the contents of the database to an XML .plist file. 454 455 The file can be passed as either a file object or a pathname. 456 All data, including included databases, is dumped.""" 457 458 packages = [] 459 for pkg in self._packages: 460 packages.append(pkg.dump()) 461 plistdata = { 462 'Version': self._version, 463 'Maintainer': self._maintainer, 464 'Description': self._description, 465 'Packages': packages 466 } 467 plist = plistlib.Plist(**plistdata) 468 plist.write(pathOrFile) 469 470 def find(self, ident): 471 """Find a package. The package can be specified by name 472 or as a dictionary with name, version and flavor entries. 473 474 Only name is obligatory. If there are multiple matches the 475 best one (higher version number, flavors ordered according to 476 users' preference) is returned.""" 477 478 if type(ident) == str: 479 # Remove ( and ) for pseudo-packages 480 if ident[0] == '(' and ident[-1] == ')': 481 ident = ident[1:-1] 482 # Split into name-version-flavor 483 fields = ident.split('-') 484 if len(fields) < 1 or len(fields) > 3: 485 return None 486 name = fields[0] 487 if len(fields) > 1: 488 version = fields[1] 489 else: 490 version = None 491 if len(fields) > 2: 492 flavor = fields[2] 493 else: 494 flavor = None 495 else: 496 name = ident['Name'] 497 version = ident.get('Version') 498 flavor = ident.get('Flavor') 499 found = None 500 for p in self._packages: 501 if name == p.name() and \ 502 (not version or version == p.version()) and \ 503 (not flavor or flavor == p.flavor()): 504 if not found or found < p: 505 found = p 506 return found 507 508 ALLOWED_KEYS = [ 509 "Name", 510 "Version", 511 "Flavor", 512 "Description", 513 "Home-page", 514 "Download-URL", 515 "Install-test", 516 "Install-command", 517 "Pre-install-command", 518 "Post-install-command", 519 "Prerequisites", 520 "MD5Sum", 521 "User-install-skips", 522 "Systemwide-only", 523 ] 524 525 class PimpPackage: 526 """Class representing a single package.""" 527 528 def __init__(self, db, plistdata): 529 self._db = db 530 name = plistdata["Name"] 531 for k in plistdata.keys(): 532 if not k in ALLOWED_KEYS: 533 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k)) 534 self._dict = plistdata 535 536 def __getitem__(self, key): 537 return self._dict[key] 538 539 def name(self): return self._dict['Name'] 540 def version(self): return self._dict.get('Version') 541 def flavor(self): return self._dict.get('Flavor') 542 def description(self): return self._dict['Description'].strip() 543 def shortdescription(self): return self.description().splitlines()[0] 544 def homepage(self): return self._dict.get('Home-page') 545 def downloadURL(self): return self._dict.get('Download-URL') 546 def systemwideOnly(self): return self._dict.get('Systemwide-only') 547 548 def fullname(self): 549 """Return the full name "name-version-flavor" of a package. 550 551 If the package is a pseudo-package, something that cannot be 552 installed through pimp, return the name in (parentheses).""" 553 554 rv = self._dict['Name'] 555 if 'Version' in self._dict: 556 rv = rv + '-%s' % self._dict['Version'] 557 if 'Flavor' in self._dict: 558 rv = rv + '-%s' % self._dict['Flavor'] 559 if self._dict.get('Flavor') == 'hidden': 560 # Pseudo-package, show in parentheses 561 rv = '(%s)' % rv 562 return rv 563 564 def dump(self): 565 """Return a dict object containing the information on the package.""" 566 return self._dict 567 568 def __cmp__(self, other): 569 """Compare two packages, where the "better" package sorts lower.""" 570 571 if not isinstance(other, PimpPackage): 572 return cmp(id(self), id(other)) 573 if self.name() != other.name(): 574 return cmp(self.name(), other.name()) 575 if self.version() != other.version(): 576 return -cmp(self.version(), other.version()) 577 return self._db.preferences.compareFlavors(self.flavor(), other.flavor()) 578 579 def installed(self): 580 """Test wheter the package is installed. 581 582 Returns two values: a status indicator which is one of 583 "yes", "no", "old" (an older version is installed) or "bad" 584 (something went wrong during the install test) and a human 585 readable string which may contain more details.""" 586 587 namespace = { 588 "NotInstalled": _scriptExc_NotInstalled, 589 "OldInstalled": _scriptExc_OldInstalled, 590 "BadInstalled": _scriptExc_BadInstalled, 591 "os": os, 592 "sys": sys, 593 } 594 installTest = self._dict['Install-test'].strip() + '\n' 595 try: 596 exec installTest in namespace 597 except ImportError, arg: 598 return "no", str(arg) 599 except _scriptExc_NotInstalled, arg: 600 return "no", str(arg) 601 except _scriptExc_OldInstalled, arg: 602 return "old", str(arg) 603 except _scriptExc_BadInstalled, arg: 604 return "bad", str(arg) 605 except: 606 sys.stderr.write("-------------------------------------\n") 607 sys.stderr.write("---- %s: install test got exception\n" % self.fullname()) 608 sys.stderr.write("---- source:\n") 609 sys.stderr.write(installTest) 610 sys.stderr.write("---- exception:\n") 611 import traceback 612 traceback.print_exc(file=sys.stderr) 613 if self._db._maintainer: 614 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer) 615 sys.stderr.write("-------------------------------------\n") 616 return "bad", "Package install test got exception" 617 return "yes", "" 618 619 def prerequisites(self): 620 """Return a list of prerequisites for this package. 621 622 The list contains 2-tuples, of which the first item is either 623 a PimpPackage object or None, and the second is a descriptive 624 string. The first item can be None if this package depends on 625 something that isn't pimp-installable, in which case the descriptive 626 string should tell the user what to do.""" 627 628 rv = [] 629 if not self._dict.get('Download-URL'): 630 # For pseudo-packages that are already installed we don't 631 # return an error message 632 status, _ = self.installed() 633 if status == "yes": 634 return [] 635 return [(None, 636 "Package %s cannot be installed automatically, see the description" % 637 self.fullname())] 638 if self.systemwideOnly() and self._db.preferences.isUserInstall(): 639 return [(None, 640 "Package %s can only be installed system-wide" % 641 self.fullname())] 642 if not self._dict.get('Prerequisites'): 643 return [] 644 for item in self._dict['Prerequisites']: 645 if type(item) == str: 646 pkg = None 647 descr = str(item) 648 else: 649 name = item['Name'] 650 if 'Version' in item: 651 name = name + '-' + item['Version'] 652 if 'Flavor' in item: 653 name = name + '-' + item['Flavor'] 654 pkg = self._db.find(name) 655 if not pkg: 656 descr = "Requires unknown %s"%name 657 else: 658 descr = pkg.shortdescription() 659 rv.append((pkg, descr)) 660 return rv 661 662 663 def downloadPackageOnly(self, output=None): 664 """Download a single package, if needed. 665 666 An MD5 signature is used to determine whether download is needed, 667 and to test that we actually downloaded what we expected. 668 If output is given it is a file-like object that will receive a log 669 of what happens. 670 671 If anything unforeseen happened the method returns an error message 672 string. 673 """ 674 675 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL']) 676 path = urllib.url2pathname(path) 677 filename = os.path.split(path)[1] 678 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename) 679 if not self._archiveOK(): 680 if scheme == 'manual': 681 return "Please download package manually and save as %s" % self.archiveFilename 682 downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir, 683 watcher=self._db.preferences.watcher) 684 if not downloader.download(self._dict['Download-URL'], 685 self.archiveFilename, output): 686 return "download command failed" 687 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE: 688 return "archive not found after download" 689 if not self._archiveOK(): 690 return "archive does not have correct MD5 checksum" 691 692 def _archiveOK(self): 693 """Test an archive. It should exist and the MD5 checksum should be correct.""" 694 695 if not os.path.exists(self.archiveFilename): 696 return 0 697 if not self._dict.get('MD5Sum'): 698 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname()) 699 return 1 700 data = open(self.archiveFilename, 'rb').read() 701 checksum = hashlib.md5(data).hexdigest() 702 return checksum == self._dict['MD5Sum'] 703 704 def unpackPackageOnly(self, output=None): 705 """Unpack a downloaded package archive.""" 706 707 filename = os.path.split(self.archiveFilename)[1] 708 for ext, unpackerClass, arg in ARCHIVE_FORMATS: 709 if filename[-len(ext):] == ext: 710 break 711 else: 712 return "unknown extension for archive file: %s" % filename 713 self.basename = filename[:-len(ext)] 714 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir, 715 watcher=self._db.preferences.watcher) 716 rv = unpacker.unpack(self.archiveFilename, output=output) 717 if rv: 718 return rv 719 720 def installPackageOnly(self, output=None): 721 """Default install method, to be overridden by subclasses""" 722 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \ 723 % (self.fullname(), self._dict.get(flavor, "")) 724 725 def installSinglePackage(self, output=None): 726 """Download, unpack and install a single package. 727 728 If output is given it should be a file-like object and it 729 will receive a log of what happened.""" 730 731 if not self._dict.get('Download-URL'): 732 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname() 733 msg = self.downloadPackageOnly(output) 734 if msg: 735 return "%s: download: %s" % (self.fullname(), msg) 736 737 msg = self.unpackPackageOnly(output) 738 if msg: 739 return "%s: unpack: %s" % (self.fullname(), msg) 740 741 return self.installPackageOnly(output) 742 743 def beforeInstall(self): 744 """Bookkeeping before installation: remember what we have in site-packages""" 745 self._old_contents = os.listdir(self._db.preferences.installDir) 746 747 def afterInstall(self): 748 """Bookkeeping after installation: interpret any new .pth files that have 749 appeared""" 750 751 new_contents = os.listdir(self._db.preferences.installDir) 752 for fn in new_contents: 753 if fn in self._old_contents: 754 continue 755 if fn[-4:] != '.pth': 756 continue 757 fullname = os.path.join(self._db.preferences.installDir, fn) 758 f = open(fullname) 759 for line in f.readlines(): 760 if not line: 761 continue 762 if line[0] == '#': 763 continue 764 if line[:6] == 'import': 765 exec line 766 continue 767 if line[-1] == '\n': 768 line = line[:-1] 769 if not os.path.isabs(line): 770 line = os.path.join(self._db.preferences.installDir, line) 771 line = os.path.realpath(line) 772 if not line in sys.path: 773 sys.path.append(line) 774 775 def filterExpectedSkips(self, names): 776 """Return a list that contains only unpexpected skips""" 777 if not self._db.preferences.isUserInstall(): 778 return names 779 expected_skips = self._dict.get('User-install-skips') 780 if not expected_skips: 781 return names 782 newnames = [] 783 for name in names: 784 for skip in expected_skips: 785 if name[:len(skip)] == skip: 786 break 787 else: 788 newnames.append(name) 789 return newnames 790 791 class PimpPackage_binary(PimpPackage): 792 793 def unpackPackageOnly(self, output=None): 794 """We don't unpack binary packages until installing""" 795 pass 796 797 def installPackageOnly(self, output=None): 798 """Install a single source package. 799 800 If output is given it should be a file-like object and it 801 will receive a log of what happened.""" 802 803 if 'Install-command' in self._dict: 804 return "%s: Binary package cannot have Install-command" % self.fullname() 805 806 if 'Pre-install-command' in self._dict: 807 if _cmd(output, '/tmp', self._dict['Pre-install-command']): 808 return "pre-install %s: running \"%s\" failed" % \ 809 (self.fullname(), self._dict['Pre-install-command']) 810 811 self.beforeInstall() 812 813 # Install by unpacking 814 filename = os.path.split(self.archiveFilename)[1] 815 for ext, unpackerClass, arg in ARCHIVE_FORMATS: 816 if filename[-len(ext):] == ext: 817 break 818 else: 819 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename) 820 self.basename = filename[:-len(ext)] 821 822 install_renames = [] 823 for k, newloc in self._db.preferences.installLocations: 824 if not newloc: 825 continue 826 if k == "--install-lib": 827 oldloc = DEFAULT_INSTALLDIR 828 else: 829 return "%s: Don't know installLocation %s" % (self.fullname(), k) 830 install_renames.append((oldloc, newloc)) 831 832 unpacker = unpackerClass(arg, dir="/", renames=install_renames) 833 rv = unpacker.unpack(self.archiveFilename, output=output, package=self) 834 if rv: 835 return rv 836 837 self.afterInstall() 838 839 if 'Post-install-command' in self._dict: 840 if _cmd(output, '/tmp', self._dict['Post-install-command']): 841 return "%s: post-install: running \"%s\" failed" % \ 842 (self.fullname(), self._dict['Post-install-command']) 843 844 return None 845 846 847 class PimpPackage_source(PimpPackage): 848 849 def unpackPackageOnly(self, output=None): 850 """Unpack a source package and check that setup.py exists""" 851 PimpPackage.unpackPackageOnly(self, output) 852 # Test that a setup script has been create 853 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename) 854 setupname = os.path.join(self._buildDirname, "setup.py") 855 if not os.path.exists(setupname) and not NO_EXECUTE: 856 return "no setup.py found after unpack of archive" 857 858 def installPackageOnly(self, output=None): 859 """Install a single source package. 860 861 If output is given it should be a file-like object and it 862 will receive a log of what happened.""" 863 864 if 'Pre-install-command' in self._dict: 865 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']): 866 return "pre-install %s: running \"%s\" failed" % \ 867 (self.fullname(), self._dict['Pre-install-command']) 868 869 self.beforeInstall() 870 installcmd = self._dict.get('Install-command') 871 if installcmd and self._install_renames: 872 return "Package has install-command and can only be installed to standard location" 873 # This is the "bit-bucket" for installations: everything we don't 874 # want. After installation we check that it is actually empty 875 unwanted_install_dir = None 876 if not installcmd: 877 extra_args = "" 878 for k, v in self._db.preferences.installLocations: 879 if not v: 880 # We don't want these files installed. Send them 881 # to the bit-bucket. 882 if not unwanted_install_dir: 883 unwanted_install_dir = tempfile.mkdtemp() 884 v = unwanted_install_dir 885 extra_args = extra_args + " %s \"%s\"" % (k, v) 886 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args) 887 if _cmd(output, self._buildDirname, installcmd): 888 return "install %s: running \"%s\" failed" % \ 889 (self.fullname(), installcmd) 890 if unwanted_install_dir and os.path.exists(unwanted_install_dir): 891 unwanted_files = os.listdir(unwanted_install_dir) 892 if unwanted_files: 893 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files) 894 else: 895 rv = None 896 shutil.rmtree(unwanted_install_dir) 897 return rv 898 899 self.afterInstall() 900 901 if 'Post-install-command' in self._dict: 902 if _cmd(output, self._buildDirname, self._dict['Post-install-command']): 903 return "post-install %s: running \"%s\" failed" % \ 904 (self.fullname(), self._dict['Post-install-command']) 905 return None 906 907 class PimpPackage_installer(PimpPackage): 908 909 def unpackPackageOnly(self, output=None): 910 """We don't unpack dmg packages until installing""" 911 pass 912 913 def installPackageOnly(self, output=None): 914 """Install a single source package. 915 916 If output is given it should be a file-like object and it 917 will receive a log of what happened.""" 918 919 if 'Post-install-command' in self._dict: 920 return "%s: Installer package cannot have Post-install-command" % self.fullname() 921 922 if 'Pre-install-command' in self._dict: 923 if _cmd(output, '/tmp', self._dict['Pre-install-command']): 924 return "pre-install %s: running \"%s\" failed" % \ 925 (self.fullname(), self._dict['Pre-install-command']) 926 927 self.beforeInstall() 928 929 installcmd = self._dict.get('Install-command') 930 if installcmd: 931 if '%' in installcmd: 932 installcmd = installcmd % self.archiveFilename 933 else: 934 installcmd = 'open \"%s\"' % self.archiveFilename 935 if _cmd(output, "/tmp", installcmd): 936 return '%s: install command failed (use verbose for details)' % self.fullname() 937 return '%s: downloaded and opened. Install manually and restart Package Manager' % self.archiveFilename 938 939 class PimpInstaller: 940 """Installer engine: computes dependencies and installs 941 packages in the right order.""" 942 943 def __init__(self, db): 944 self._todo = [] 945 self._db = db 946 self._curtodo = [] 947 self._curmessages = [] 948 949 def __contains__(self, package): 950 return package in self._todo 951 952 def _addPackages(self, packages): 953 for package in packages: 954 if not package in self._todo: 955 self._todo.append(package) 956 957 def _prepareInstall(self, package, force=0, recursive=1): 958 """Internal routine, recursive engine for prepareInstall. 959 960 Test whether the package is installed and (if not installed 961 or if force==1) prepend it to the temporary todo list and 962 call ourselves recursively on all prerequisites.""" 963 964 if not force: 965 status, message = package.installed() 966 if status == "yes": 967 return 968 if package in self._todo or package in self._curtodo: 969 return 970 self._curtodo.insert(0, package) 971 if not recursive: 972 return 973 prereqs = package.prerequisites() 974 for pkg, descr in prereqs: 975 if pkg: 976 self._prepareInstall(pkg, False, recursive) 977 else: 978 self._curmessages.append("Problem with dependency: %s" % descr) 979 980 def prepareInstall(self, package, force=0, recursive=1): 981 """Prepare installation of a package. 982 983 If the package is already installed and force is false nothing 984 is done. If recursive is true prerequisites are installed first. 985 986 Returns a list of packages (to be passed to install) and a list 987 of messages of any problems encountered. 988 """ 989 990 self._curtodo = [] 991 self._curmessages = [] 992 self._prepareInstall(package, force, recursive) 993 rv = self._curtodo, self._curmessages 994 self._curtodo = [] 995 self._curmessages = [] 996 return rv 997 998 def install(self, packages, output): 999 """Install a list of packages.""" 1000 1001 self._addPackages(packages) 1002 status = [] 1003 for pkg in self._todo: 1004 msg = pkg.installSinglePackage(output) 1005 if msg: 1006 status.append(msg) 1007 return status 1008 1009 1010 1011 def _run(mode, verbose, force, args, prefargs, watcher): 1012 """Engine for the main program""" 1013 1014 prefs = PimpPreferences(**prefargs) 1015 if watcher: 1016 prefs.setWatcher(watcher) 1017 rv = prefs.check() 1018 if rv: 1019 sys.stdout.write(rv) 1020 db = PimpDatabase(prefs) 1021 db.appendURL(prefs.pimpDatabase) 1022 1023 if mode == 'dump': 1024 db.dump(sys.stdout) 1025 elif mode =='list': 1026 if not args: 1027 args = db.listnames() 1028 print "%-20.20s\t%s" % ("Package", "Description") 1029 print 1030 for pkgname in args: 1031 pkg = db.find(pkgname) 1032 if pkg: 1033 description = pkg.shortdescription() 1034 pkgname = pkg.fullname() 1035 else: 1036 description = 'Error: no such package' 1037 print "%-20.20s\t%s" % (pkgname, description) 1038 if verbose: 1039 print "\tHome page:\t", pkg.homepage() 1040 try: 1041 print "\tDownload URL:\t", pkg.downloadURL() 1042 except KeyError: 1043 pass 1044 description = pkg.description() 1045 description = '\n\t\t\t\t\t'.join(description.splitlines()) 1046 print "\tDescription:\t%s" % description 1047 elif mode =='status': 1048 if not args: 1049 args = db.listnames() 1050 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message") 1051 print 1052 for pkgname in args: 1053 pkg = db.find(pkgname) 1054 if pkg: 1055 status, msg = pkg.installed() 1056 pkgname = pkg.fullname() 1057 else: 1058 status = 'error' 1059 msg = 'No such package' 1060 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg) 1061 if verbose and status == "no": 1062 prereq = pkg.prerequisites() 1063 for pkg, msg in prereq: 1064 if not pkg: 1065 pkg = '' 1066 else: 1067 pkg = pkg.fullname() 1068 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg) 1069 elif mode == 'install': 1070 if not args: 1071 print 'Please specify packages to install' 1072 sys.exit(1) 1073 inst = PimpInstaller(db) 1074 for pkgname in args: 1075 pkg = db.find(pkgname) 1076 if not pkg: 1077 print '%s: No such package' % pkgname 1078 continue 1079 list, messages = inst.prepareInstall(pkg, force) 1080 if messages and not force: 1081 print "%s: Not installed:" % pkgname 1082 for m in messages: 1083 print "\t", m 1084 else: 1085 if verbose: 1086 output = sys.stdout 1087 else: 1088 output = None 1089 messages = inst.install(list, output) 1090 if messages: 1091 print "%s: Not installed:" % pkgname 1092 for m in messages: 1093 print "\t", m 1094 1095 def main(): 1096 """Minimal commandline tool to drive pimp.""" 1097 1098 import getopt 1099 def _help(): 1100 print "Usage: pimp [options] -s [package ...] List installed status" 1101 print " pimp [options] -l [package ...] Show package information" 1102 print " pimp [options] -i package ... Install packages" 1103 print " pimp -d Dump database to stdout" 1104 print " pimp -V Print version number" 1105 print "Options:" 1106 print " -v Verbose" 1107 print " -f Force installation" 1108 print " -D dir Set destination directory" 1109 print " (default: %s)" % DEFAULT_INSTALLDIR 1110 print " -u url URL for database" 1111 sys.exit(1) 1112 1113 class _Watcher: 1114 def update(self, msg): 1115 sys.stderr.write(msg + '\r') 1116 return 1 1117 1118 try: 1119 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:") 1120 except getopt.GetoptError: 1121 _help() 1122 if not opts and not args: 1123 _help() 1124 mode = None 1125 force = 0 1126 verbose = 0 1127 prefargs = {} 1128 watcher = None 1129 for o, a in opts: 1130 if o == '-s': 1131 if mode: 1132 _help() 1133 mode = 'status' 1134 if o == '-l': 1135 if mode: 1136 _help() 1137 mode = 'list' 1138 if o == '-d': 1139 if mode: 1140 _help() 1141 mode = 'dump' 1142 if o == '-V': 1143 if mode: 1144 _help() 1145 mode = 'version' 1146 if o == '-i': 1147 mode = 'install' 1148 if o == '-f': 1149 force = 1 1150 if o == '-v': 1151 verbose = 1 1152 watcher = _Watcher() 1153 if o == '-D': 1154 prefargs['installDir'] = a 1155 if o == '-u': 1156 prefargs['pimpDatabase'] = a 1157 if not mode: 1158 _help() 1159 if mode == 'version': 1160 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__) 1161 else: 1162 _run(mode, verbose, force, args, prefargs, watcher) 1163 1164 # Finally, try to update ourselves to a newer version. 1165 # If the end-user updates pimp through pimp the new version 1166 # will be called pimp_update and live in site-packages 1167 # or somewhere similar 1168 if __name__ != 'pimp_update': 1169 try: 1170 import pimp_update 1171 except ImportError: 1172 pass 1173 else: 1174 if pimp_update.PIMP_VERSION <= PIMP_VERSION: 1175 import warnings 1176 warnings.warn("pimp_update is version %s, not newer than pimp version %s" % 1177 (pimp_update.PIMP_VERSION, PIMP_VERSION)) 1178 else: 1179 from pimp_update import * 1180 1181 if __name__ == '__main__': 1182 main() 1183