diff options
Diffstat (limited to 'lib/python2.7/plat-mac/pimp.py')
-rw-r--r-- | lib/python2.7/plat-mac/pimp.py | 1182 |
1 files changed, 1182 insertions, 0 deletions
diff --git a/lib/python2.7/plat-mac/pimp.py b/lib/python2.7/plat-mac/pimp.py new file mode 100644 index 0000000..9a3e82e --- /dev/null +++ b/lib/python2.7/plat-mac/pimp.py @@ -0,0 +1,1182 @@ +"""Package Install Manager for Python. + +This is currently a MacOSX-only strawman implementation. +Despite other rumours the name stands for "Packman IMPlementation". + +Tools to allow easy installation of packages. The idea is that there is +an online XML database per (platform, python-version) containing packages +known to work with that combination. This module contains tools for getting +and parsing the database, testing whether packages are installed, computing +dependencies and installing packages. + +There is a minimal main program that works as a command line tool, but the +intention is that the end user will use this through a GUI. +""" + +from warnings import warnpy3k +warnpy3k("In 3.x, the pimp module is removed.", stacklevel=2) + +import sys +import os +import subprocess +import urllib +import urllib2 +import urlparse +import plistlib +import distutils.util +import distutils.sysconfig +import hashlib +import tarfile +import tempfile +import shutil +import time + +__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main", + "getDefaultDatabase", "PIMP_VERSION", "main"] + +_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled" +_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled" +_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled" + +NO_EXECUTE=0 + +PIMP_VERSION="0.5" + +# Flavors: +# source: setup-based package +# binary: tar (or other) archive created with setup.py bdist. +# installer: something that can be opened +DEFAULT_FLAVORORDER=['source', 'binary', 'installer'] +DEFAULT_DOWNLOADDIR='/tmp' +DEFAULT_BUILDDIR='/tmp' +DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib() +DEFAULT_PIMPDATABASE_FMT="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist" + +def getDefaultDatabase(experimental=False): + if experimental: + status = "exp" + else: + status = "prod" + + major, minor, micro, state, extra = sys.version_info + pyvers = '%d.%d' % (major, minor) + if micro == 0 and state != 'final': + pyvers = pyvers + '%s%d' % (state, extra) + + longplatform = distutils.util.get_platform() + osname, release, machine = longplatform.split('-') + # For some platforms we may want to differentiate between + # installation types + if osname == 'darwin': + if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'): + osname = 'darwin_apple' + elif sys.prefix.startswith('/Library/Frameworks/Python.framework'): + osname = 'darwin_macpython' + # Otherwise we don't know... + # Now we try various URLs by playing with the release string. + # We remove numbers off the end until we find a match. + rel = release + while True: + url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine) + try: + urllib2.urlopen(url) + except urllib2.HTTPError, arg: + pass + else: + break + if not rel: + # We're out of version numbers to try. Use the + # full release number, this will give a reasonable + # error message later + url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine) + break + idx = rel.rfind('.') + if idx < 0: + rel = '' + else: + rel = rel[:idx] + return url + +def _cmd(output, dir, *cmditems): + """Internal routine to run a shell command in a given directory.""" + + cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems) + if output: + output.write("+ %s\n" % cmd) + if NO_EXECUTE: + return 0 + child = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + child.stdin.close() + while 1: + line = child.stdout.readline() + if not line: + break + if output: + output.write(line) + return child.wait() + +class PimpDownloader: + """Abstract base class - Downloader for archives""" + + def __init__(self, argument, + dir="", + watcher=None): + self.argument = argument + self._dir = dir + self._watcher = watcher + + def download(self, url, filename, output=None): + return None + + def update(self, str): + if self._watcher: + return self._watcher.update(str) + return True + +class PimpCurlDownloader(PimpDownloader): + + def download(self, url, filename, output=None): + self.update("Downloading %s..." % url) + exitstatus = _cmd(output, self._dir, + "curl", + "--output", filename, + url) + self.update("Downloading %s: finished" % url) + return (not exitstatus) + +class PimpUrllibDownloader(PimpDownloader): + + def download(self, url, filename, output=None): + output = open(filename, 'wb') + self.update("Downloading %s: opening connection" % url) + keepgoing = True + download = urllib2.urlopen(url) + if 'content-length' in download.headers: + length = long(download.headers['content-length']) + else: + length = -1 + + data = download.read(4096) #read 4K at a time + dlsize = 0 + lasttime = 0 + while keepgoing: + dlsize = dlsize + len(data) + if len(data) == 0: + #this is our exit condition + break + output.write(data) + if int(time.time()) != lasttime: + # Update at most once per second + lasttime = int(time.time()) + if length == -1: + keepgoing = self.update("Downloading %s: %d bytes..." % (url, dlsize)) + else: + keepgoing = self.update("Downloading %s: %d%% (%d bytes)..." % (url, int(100.0*dlsize/length), dlsize)) + data = download.read(4096) + if keepgoing: + self.update("Downloading %s: finished" % url) + return keepgoing + +class PimpUnpacker: + """Abstract base class - Unpacker for archives""" + + _can_rename = False + + def __init__(self, argument, + dir="", + renames=[], + watcher=None): + self.argument = argument + if renames and not self._can_rename: + raise RuntimeError, "This unpacker cannot rename files" + self._dir = dir + self._renames = renames + self._watcher = watcher + + def unpack(self, archive, output=None, package=None): + return None + + def update(self, str): + if self._watcher: + return self._watcher.update(str) + return True + +class PimpCommandUnpacker(PimpUnpacker): + """Unpack archives by calling a Unix utility""" + + _can_rename = False + + def unpack(self, archive, output=None, package=None): + cmd = self.argument % archive + if _cmd(output, self._dir, cmd): + return "unpack command failed" + +class PimpTarUnpacker(PimpUnpacker): + """Unpack tarfiles using the builtin tarfile module""" + + _can_rename = True + + def unpack(self, archive, output=None, package=None): + tf = tarfile.open(archive, "r") + members = tf.getmembers() + skip = [] + if self._renames: + for member in members: + for oldprefix, newprefix in self._renames: + if oldprefix[:len(self._dir)] == self._dir: + oldprefix2 = oldprefix[len(self._dir):] + else: + oldprefix2 = None + if member.name[:len(oldprefix)] == oldprefix: + if newprefix is None: + skip.append(member) + #print 'SKIP', member.name + else: + member.name = newprefix + member.name[len(oldprefix):] + print ' ', member.name + break + elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2: + if newprefix is None: + skip.append(member) + #print 'SKIP', member.name + else: + member.name = newprefix + member.name[len(oldprefix2):] + #print ' ', member.name + break + else: + skip.append(member) + #print '????', member.name + for member in members: + if member in skip: + self.update("Skipping %s" % member.name) + continue + self.update("Extracting %s" % member.name) + tf.extract(member, self._dir) + if skip: + names = [member.name for member in skip if member.name[-1] != '/'] + if package: + names = package.filterExpectedSkips(names) + if names: + return "Not all files were unpacked: %s" % " ".join(names) + +ARCHIVE_FORMATS = [ + (".tar.Z", PimpTarUnpacker, None), + (".taz", PimpTarUnpacker, None), + (".tar.gz", PimpTarUnpacker, None), + (".tgz", PimpTarUnpacker, None), + (".tar.bz", PimpTarUnpacker, None), + (".zip", PimpCommandUnpacker, "unzip \"%s\""), +] + +class PimpPreferences: + """Container for per-user preferences, such as the database to use + and where to install packages.""" + + def __init__(self, + flavorOrder=None, + downloadDir=None, + buildDir=None, + installDir=None, + pimpDatabase=None): + if not flavorOrder: + flavorOrder = DEFAULT_FLAVORORDER + if not downloadDir: + downloadDir = DEFAULT_DOWNLOADDIR + if not buildDir: + buildDir = DEFAULT_BUILDDIR + if not pimpDatabase: + pimpDatabase = getDefaultDatabase() + self.setInstallDir(installDir) + self.flavorOrder = flavorOrder + self.downloadDir = downloadDir + self.buildDir = buildDir + self.pimpDatabase = pimpDatabase + self.watcher = None + + def setWatcher(self, watcher): + self.watcher = watcher + + def setInstallDir(self, installDir=None): + if installDir: + # Installing to non-standard location. + self.installLocations = [ + ('--install-lib', installDir), + ('--install-headers', None), + ('--install-scripts', None), + ('--install-data', None)] + else: + installDir = DEFAULT_INSTALLDIR + self.installLocations = [] + self.installDir = installDir + + def isUserInstall(self): + return self.installDir != DEFAULT_INSTALLDIR + + def check(self): + """Check that the preferences make sense: directories exist and are + writable, the install directory is on sys.path, etc.""" + + rv = "" + RWX_OK = os.R_OK|os.W_OK|os.X_OK + if not os.path.exists(self.downloadDir): + rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir + elif not os.access(self.downloadDir, RWX_OK): + rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir + if not os.path.exists(self.buildDir): + rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir + elif not os.access(self.buildDir, RWX_OK): + rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir + if not os.path.exists(self.installDir): + rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir + elif not os.access(self.installDir, RWX_OK): + rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir + else: + installDir = os.path.realpath(self.installDir) + for p in sys.path: + try: + realpath = os.path.realpath(p) + except: + pass + if installDir == realpath: + break + else: + rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir + return rv + + def compareFlavors(self, left, right): + """Compare two flavor strings. This is part of your preferences + because whether the user prefers installing from source or binary is.""" + if left in self.flavorOrder: + if right in self.flavorOrder: + return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right)) + return -1 + if right in self.flavorOrder: + return 1 + return cmp(left, right) + +class PimpDatabase: + """Class representing a pimp database. It can actually contain + information from multiple databases through inclusion, but the + toplevel database is considered the master, as its maintainer is + "responsible" for the contents.""" + + def __init__(self, prefs): + self._packages = [] + self.preferences = prefs + self._url = "" + self._urllist = [] + self._version = "" + self._maintainer = "" + self._description = "" + + # Accessor functions + def url(self): return self._url + def version(self): return self._version + def maintainer(self): return self._maintainer + def description(self): return self._description + + def close(self): + """Clean up""" + self._packages = [] + self.preferences = None + + def appendURL(self, url, included=0): + """Append packages from the database with the given URL. + Only the first database should specify included=0, so the + global information (maintainer, description) get stored.""" + + if url in self._urllist: + return + self._urllist.append(url) + fp = urllib2.urlopen(url).fp + plistdata = plistlib.Plist.fromFile(fp) + # Test here for Pimp version, etc + if included: + version = plistdata.get('Version') + if version and version > self._version: + sys.stderr.write("Warning: included database %s is for pimp version %s\n" % + (url, version)) + else: + self._version = plistdata.get('Version') + if not self._version: + sys.stderr.write("Warning: database has no Version information\n") + elif self._version > PIMP_VERSION: + sys.stderr.write("Warning: database version %s newer than pimp version %s\n" + % (self._version, PIMP_VERSION)) + self._maintainer = plistdata.get('Maintainer', '') + self._description = plistdata.get('Description', '').strip() + self._url = url + self._appendPackages(plistdata['Packages'], url) + others = plistdata.get('Include', []) + for o in others: + o = urllib.basejoin(url, o) + self.appendURL(o, included=1) + + def _appendPackages(self, packages, url): + """Given a list of dictionaries containing package + descriptions create the PimpPackage objects and append them + to our internal storage.""" + + for p in packages: + p = dict(p) + if 'Download-URL' in p: + p['Download-URL'] = urllib.basejoin(url, p['Download-URL']) + flavor = p.get('Flavor') + if flavor == 'source': + pkg = PimpPackage_source(self, p) + elif flavor == 'binary': + pkg = PimpPackage_binary(self, p) + elif flavor == 'installer': + pkg = PimpPackage_installer(self, p) + elif flavor == 'hidden': + pkg = PimpPackage_installer(self, p) + else: + pkg = PimpPackage(self, dict(p)) + self._packages.append(pkg) + + def list(self): + """Return a list of all PimpPackage objects in the database.""" + + return self._packages + + def listnames(self): + """Return a list of names of all packages in the database.""" + + rv = [] + for pkg in self._packages: + rv.append(pkg.fullname()) + rv.sort() + return rv + + def dump(self, pathOrFile): + """Dump the contents of the database to an XML .plist file. + + The file can be passed as either a file object or a pathname. + All data, including included databases, is dumped.""" + + packages = [] + for pkg in self._packages: + packages.append(pkg.dump()) + plistdata = { + 'Version': self._version, + 'Maintainer': self._maintainer, + 'Description': self._description, + 'Packages': packages + } + plist = plistlib.Plist(**plistdata) + plist.write(pathOrFile) + + def find(self, ident): + """Find a package. The package can be specified by name + or as a dictionary with name, version and flavor entries. + + Only name is obligatory. If there are multiple matches the + best one (higher version number, flavors ordered according to + users' preference) is returned.""" + + if type(ident) == str: + # Remove ( and ) for pseudo-packages + if ident[0] == '(' and ident[-1] == ')': + ident = ident[1:-1] + # Split into name-version-flavor + fields = ident.split('-') + if len(fields) < 1 or len(fields) > 3: + return None + name = fields[0] + if len(fields) > 1: + version = fields[1] + else: + version = None + if len(fields) > 2: + flavor = fields[2] + else: + flavor = None + else: + name = ident['Name'] + version = ident.get('Version') + flavor = ident.get('Flavor') + found = None + for p in self._packages: + if name == p.name() and \ + (not version or version == p.version()) and \ + (not flavor or flavor == p.flavor()): + if not found or found < p: + found = p + return found + +ALLOWED_KEYS = [ + "Name", + "Version", + "Flavor", + "Description", + "Home-page", + "Download-URL", + "Install-test", + "Install-command", + "Pre-install-command", + "Post-install-command", + "Prerequisites", + "MD5Sum", + "User-install-skips", + "Systemwide-only", +] + +class PimpPackage: + """Class representing a single package.""" + + def __init__(self, db, plistdata): + self._db = db + name = plistdata["Name"] + for k in plistdata.keys(): + if not k in ALLOWED_KEYS: + sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k)) + self._dict = plistdata + + def __getitem__(self, key): + return self._dict[key] + + def name(self): return self._dict['Name'] + def version(self): return self._dict.get('Version') + def flavor(self): return self._dict.get('Flavor') + def description(self): return self._dict['Description'].strip() + def shortdescription(self): return self.description().splitlines()[0] + def homepage(self): return self._dict.get('Home-page') + def downloadURL(self): return self._dict.get('Download-URL') + def systemwideOnly(self): return self._dict.get('Systemwide-only') + + def fullname(self): + """Return the full name "name-version-flavor" of a package. + + If the package is a pseudo-package, something that cannot be + installed through pimp, return the name in (parentheses).""" + + rv = self._dict['Name'] + if 'Version' in self._dict: + rv = rv + '-%s' % self._dict['Version'] + if 'Flavor' in self._dict: + rv = rv + '-%s' % self._dict['Flavor'] + if self._dict.get('Flavor') == 'hidden': + # Pseudo-package, show in parentheses + rv = '(%s)' % rv + return rv + + def dump(self): + """Return a dict object containing the information on the package.""" + return self._dict + + def __cmp__(self, other): + """Compare two packages, where the "better" package sorts lower.""" + + if not isinstance(other, PimpPackage): + return cmp(id(self), id(other)) + if self.name() != other.name(): + return cmp(self.name(), other.name()) + if self.version() != other.version(): + return -cmp(self.version(), other.version()) + return self._db.preferences.compareFlavors(self.flavor(), other.flavor()) + + def installed(self): + """Test wheter the package is installed. + + Returns two values: a status indicator which is one of + "yes", "no", "old" (an older version is installed) or "bad" + (something went wrong during the install test) and a human + readable string which may contain more details.""" + + namespace = { + "NotInstalled": _scriptExc_NotInstalled, + "OldInstalled": _scriptExc_OldInstalled, + "BadInstalled": _scriptExc_BadInstalled, + "os": os, + "sys": sys, + } + installTest = self._dict['Install-test'].strip() + '\n' + try: + exec installTest in namespace + except ImportError, arg: + return "no", str(arg) + except _scriptExc_NotInstalled, arg: + return "no", str(arg) + except _scriptExc_OldInstalled, arg: + return "old", str(arg) + except _scriptExc_BadInstalled, arg: + return "bad", str(arg) + except: + sys.stderr.write("-------------------------------------\n") + sys.stderr.write("---- %s: install test got exception\n" % self.fullname()) + sys.stderr.write("---- source:\n") + sys.stderr.write(installTest) + sys.stderr.write("---- exception:\n") + import traceback + traceback.print_exc(file=sys.stderr) + if self._db._maintainer: + sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer) + sys.stderr.write("-------------------------------------\n") + return "bad", "Package install test got exception" + return "yes", "" + + def prerequisites(self): + """Return a list of prerequisites for this package. + + The list contains 2-tuples, of which the first item is either + a PimpPackage object or None, and the second is a descriptive + string. The first item can be None if this package depends on + something that isn't pimp-installable, in which case the descriptive + string should tell the user what to do.""" + + rv = [] + if not self._dict.get('Download-URL'): + # For pseudo-packages that are already installed we don't + # return an error message + status, _ = self.installed() + if status == "yes": + return [] + return [(None, + "Package %s cannot be installed automatically, see the description" % + self.fullname())] + if self.systemwideOnly() and self._db.preferences.isUserInstall(): + return [(None, + "Package %s can only be installed system-wide" % + self.fullname())] + if not self._dict.get('Prerequisites'): + return [] + for item in self._dict['Prerequisites']: + if type(item) == str: + pkg = None + descr = str(item) + else: + name = item['Name'] + if 'Version' in item: + name = name + '-' + item['Version'] + if 'Flavor' in item: + name = name + '-' + item['Flavor'] + pkg = self._db.find(name) + if not pkg: + descr = "Requires unknown %s"%name + else: + descr = pkg.shortdescription() + rv.append((pkg, descr)) + return rv + + + def downloadPackageOnly(self, output=None): + """Download a single package, if needed. + + An MD5 signature is used to determine whether download is needed, + and to test that we actually downloaded what we expected. + If output is given it is a file-like object that will receive a log + of what happens. + + If anything unforeseen happened the method returns an error message + string. + """ + + scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL']) + path = urllib.url2pathname(path) + filename = os.path.split(path)[1] + self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename) + if not self._archiveOK(): + if scheme == 'manual': + return "Please download package manually and save as %s" % self.archiveFilename + downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir, + watcher=self._db.preferences.watcher) + if not downloader.download(self._dict['Download-URL'], + self.archiveFilename, output): + return "download command failed" + if not os.path.exists(self.archiveFilename) and not NO_EXECUTE: + return "archive not found after download" + if not self._archiveOK(): + return "archive does not have correct MD5 checksum" + + def _archiveOK(self): + """Test an archive. It should exist and the MD5 checksum should be correct.""" + + if not os.path.exists(self.archiveFilename): + return 0 + if not self._dict.get('MD5Sum'): + sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname()) + return 1 + data = open(self.archiveFilename, 'rb').read() + checksum = hashlib.md5(data).hexdigest() + return checksum == self._dict['MD5Sum'] + + def unpackPackageOnly(self, output=None): + """Unpack a downloaded package archive.""" + + filename = os.path.split(self.archiveFilename)[1] + for ext, unpackerClass, arg in ARCHIVE_FORMATS: + if filename[-len(ext):] == ext: + break + else: + return "unknown extension for archive file: %s" % filename + self.basename = filename[:-len(ext)] + unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir, + watcher=self._db.preferences.watcher) + rv = unpacker.unpack(self.archiveFilename, output=output) + if rv: + return rv + + def installPackageOnly(self, output=None): + """Default install method, to be overridden by subclasses""" + return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \ + % (self.fullname(), self._dict.get(flavor, "")) + + def installSinglePackage(self, output=None): + """Download, unpack and install a single package. + + If output is given it should be a file-like object and it + will receive a log of what happened.""" + + if not self._dict.get('Download-URL'): + return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname() + msg = self.downloadPackageOnly(output) + if msg: + return "%s: download: %s" % (self.fullname(), msg) + + msg = self.unpackPackageOnly(output) + if msg: + return "%s: unpack: %s" % (self.fullname(), msg) + + return self.installPackageOnly(output) + + def beforeInstall(self): + """Bookkeeping before installation: remember what we have in site-packages""" + self._old_contents = os.listdir(self._db.preferences.installDir) + + def afterInstall(self): + """Bookkeeping after installation: interpret any new .pth files that have + appeared""" + + new_contents = os.listdir(self._db.preferences.installDir) + for fn in new_contents: + if fn in self._old_contents: + continue + if fn[-4:] != '.pth': + continue + fullname = os.path.join(self._db.preferences.installDir, fn) + f = open(fullname) + for line in f.readlines(): + if not line: + continue + if line[0] == '#': + continue + if line[:6] == 'import': + exec line + continue + if line[-1] == '\n': + line = line[:-1] + if not os.path.isabs(line): + line = os.path.join(self._db.preferences.installDir, line) + line = os.path.realpath(line) + if not line in sys.path: + sys.path.append(line) + + def filterExpectedSkips(self, names): + """Return a list that contains only unpexpected skips""" + if not self._db.preferences.isUserInstall(): + return names + expected_skips = self._dict.get('User-install-skips') + if not expected_skips: + return names + newnames = [] + for name in names: + for skip in expected_skips: + if name[:len(skip)] == skip: + break + else: + newnames.append(name) + return newnames + +class PimpPackage_binary(PimpPackage): + + def unpackPackageOnly(self, output=None): + """We don't unpack binary packages until installing""" + pass + + def installPackageOnly(self, output=None): + """Install a single source package. + + If output is given it should be a file-like object and it + will receive a log of what happened.""" + + if 'Install-command' in self._dict: + return "%s: Binary package cannot have Install-command" % self.fullname() + + if 'Pre-install-command' in self._dict: + if _cmd(output, '/tmp', self._dict['Pre-install-command']): + return "pre-install %s: running \"%s\" failed" % \ + (self.fullname(), self._dict['Pre-install-command']) + + self.beforeInstall() + + # Install by unpacking + filename = os.path.split(self.archiveFilename)[1] + for ext, unpackerClass, arg in ARCHIVE_FORMATS: + if filename[-len(ext):] == ext: + break + else: + return "%s: unknown extension for archive file: %s" % (self.fullname(), filename) + self.basename = filename[:-len(ext)] + + install_renames = [] + for k, newloc in self._db.preferences.installLocations: + if not newloc: + continue + if k == "--install-lib": + oldloc = DEFAULT_INSTALLDIR + else: + return "%s: Don't know installLocation %s" % (self.fullname(), k) + install_renames.append((oldloc, newloc)) + + unpacker = unpackerClass(arg, dir="/", renames=install_renames) + rv = unpacker.unpack(self.archiveFilename, output=output, package=self) + if rv: + return rv + + self.afterInstall() + + if 'Post-install-command' in self._dict: + if _cmd(output, '/tmp', self._dict['Post-install-command']): + return "%s: post-install: running \"%s\" failed" % \ + (self.fullname(), self._dict['Post-install-command']) + + return None + + +class PimpPackage_source(PimpPackage): + + def unpackPackageOnly(self, output=None): + """Unpack a source package and check that setup.py exists""" + PimpPackage.unpackPackageOnly(self, output) + # Test that a setup script has been create + self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename) + setupname = os.path.join(self._buildDirname, "setup.py") + if not os.path.exists(setupname) and not NO_EXECUTE: + return "no setup.py found after unpack of archive" + + def installPackageOnly(self, output=None): + """Install a single source package. + + If output is given it should be a file-like object and it + will receive a log of what happened.""" + + if 'Pre-install-command' in self._dict: + if _cmd(output, self._buildDirname, self._dict['Pre-install-command']): + return "pre-install %s: running \"%s\" failed" % \ + (self.fullname(), self._dict['Pre-install-command']) + + self.beforeInstall() + installcmd = self._dict.get('Install-command') + if installcmd and self._install_renames: + return "Package has install-command and can only be installed to standard location" + # This is the "bit-bucket" for installations: everything we don't + # want. After installation we check that it is actually empty + unwanted_install_dir = None + if not installcmd: + extra_args = "" + for k, v in self._db.preferences.installLocations: + if not v: + # We don't want these files installed. Send them + # to the bit-bucket. + if not unwanted_install_dir: + unwanted_install_dir = tempfile.mkdtemp() + v = unwanted_install_dir + extra_args = extra_args + " %s \"%s\"" % (k, v) + installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args) + if _cmd(output, self._buildDirname, installcmd): + return "install %s: running \"%s\" failed" % \ + (self.fullname(), installcmd) + if unwanted_install_dir and os.path.exists(unwanted_install_dir): + unwanted_files = os.listdir(unwanted_install_dir) + if unwanted_files: + rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files) + else: + rv = None + shutil.rmtree(unwanted_install_dir) + return rv + + self.afterInstall() + + if 'Post-install-command' in self._dict: + if _cmd(output, self._buildDirname, self._dict['Post-install-command']): + return "post-install %s: running \"%s\" failed" % \ + (self.fullname(), self._dict['Post-install-command']) + return None + +class PimpPackage_installer(PimpPackage): + + def unpackPackageOnly(self, output=None): + """We don't unpack dmg packages until installing""" + pass + + def installPackageOnly(self, output=None): + """Install a single source package. + + If output is given it should be a file-like object and it + will receive a log of what happened.""" + + if 'Post-install-command' in self._dict: + return "%s: Installer package cannot have Post-install-command" % self.fullname() + + if 'Pre-install-command' in self._dict: + if _cmd(output, '/tmp', self._dict['Pre-install-command']): + return "pre-install %s: running \"%s\" failed" % \ + (self.fullname(), self._dict['Pre-install-command']) + + self.beforeInstall() + + installcmd = self._dict.get('Install-command') + if installcmd: + if '%' in installcmd: + installcmd = installcmd % self.archiveFilename + else: + installcmd = 'open \"%s\"' % self.archiveFilename + if _cmd(output, "/tmp", installcmd): + return '%s: install command failed (use verbose for details)' % self.fullname() + return '%s: downloaded and opened. Install manually and restart Package Manager' % self.archiveFilename + +class PimpInstaller: + """Installer engine: computes dependencies and installs + packages in the right order.""" + + def __init__(self, db): + self._todo = [] + self._db = db + self._curtodo = [] + self._curmessages = [] + + def __contains__(self, package): + return package in self._todo + + def _addPackages(self, packages): + for package in packages: + if not package in self._todo: + self._todo.append(package) + + def _prepareInstall(self, package, force=0, recursive=1): + """Internal routine, recursive engine for prepareInstall. + + Test whether the package is installed and (if not installed + or if force==1) prepend it to the temporary todo list and + call ourselves recursively on all prerequisites.""" + + if not force: + status, message = package.installed() + if status == "yes": + return + if package in self._todo or package in self._curtodo: + return + self._curtodo.insert(0, package) + if not recursive: + return + prereqs = package.prerequisites() + for pkg, descr in prereqs: + if pkg: + self._prepareInstall(pkg, False, recursive) + else: + self._curmessages.append("Problem with dependency: %s" % descr) + + def prepareInstall(self, package, force=0, recursive=1): + """Prepare installation of a package. + + If the package is already installed and force is false nothing + is done. If recursive is true prerequisites are installed first. + + Returns a list of packages (to be passed to install) and a list + of messages of any problems encountered. + """ + + self._curtodo = [] + self._curmessages = [] + self._prepareInstall(package, force, recursive) + rv = self._curtodo, self._curmessages + self._curtodo = [] + self._curmessages = [] + return rv + + def install(self, packages, output): + """Install a list of packages.""" + + self._addPackages(packages) + status = [] + for pkg in self._todo: + msg = pkg.installSinglePackage(output) + if msg: + status.append(msg) + return status + + + +def _run(mode, verbose, force, args, prefargs, watcher): + """Engine for the main program""" + + prefs = PimpPreferences(**prefargs) + if watcher: + prefs.setWatcher(watcher) + rv = prefs.check() + if rv: + sys.stdout.write(rv) + db = PimpDatabase(prefs) + db.appendURL(prefs.pimpDatabase) + + if mode == 'dump': + db.dump(sys.stdout) + elif mode =='list': + if not args: + args = db.listnames() + print "%-20.20s\t%s" % ("Package", "Description") + print + for pkgname in args: + pkg = db.find(pkgname) + if pkg: + description = pkg.shortdescription() + pkgname = pkg.fullname() + else: + description = 'Error: no such package' + print "%-20.20s\t%s" % (pkgname, description) + if verbose: + print "\tHome page:\t", pkg.homepage() + try: + print "\tDownload URL:\t", pkg.downloadURL() + except KeyError: + pass + description = pkg.description() + description = '\n\t\t\t\t\t'.join(description.splitlines()) + print "\tDescription:\t%s" % description + elif mode =='status': + if not args: + args = db.listnames() + print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message") + print + for pkgname in args: + pkg = db.find(pkgname) + if pkg: + status, msg = pkg.installed() + pkgname = pkg.fullname() + else: + status = 'error' + msg = 'No such package' + print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg) + if verbose and status == "no": + prereq = pkg.prerequisites() + for pkg, msg in prereq: + if not pkg: + pkg = '' + else: + pkg = pkg.fullname() + print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg) + elif mode == 'install': + if not args: + print 'Please specify packages to install' + sys.exit(1) + inst = PimpInstaller(db) + for pkgname in args: + pkg = db.find(pkgname) + if not pkg: + print '%s: No such package' % pkgname + continue + list, messages = inst.prepareInstall(pkg, force) + if messages and not force: + print "%s: Not installed:" % pkgname + for m in messages: + print "\t", m + else: + if verbose: + output = sys.stdout + else: + output = None + messages = inst.install(list, output) + if messages: + print "%s: Not installed:" % pkgname + for m in messages: + print "\t", m + +def main(): + """Minimal commandline tool to drive pimp.""" + + import getopt + def _help(): + print "Usage: pimp [options] -s [package ...] List installed status" + print " pimp [options] -l [package ...] Show package information" + print " pimp [options] -i package ... Install packages" + print " pimp -d Dump database to stdout" + print " pimp -V Print version number" + print "Options:" + print " -v Verbose" + print " -f Force installation" + print " -D dir Set destination directory" + print " (default: %s)" % DEFAULT_INSTALLDIR + print " -u url URL for database" + sys.exit(1) + + class _Watcher: + def update(self, msg): + sys.stderr.write(msg + '\r') + return 1 + + try: + opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:") + except getopt.GetoptError: + _help() + if not opts and not args: + _help() + mode = None + force = 0 + verbose = 0 + prefargs = {} + watcher = None + for o, a in opts: + if o == '-s': + if mode: + _help() + mode = 'status' + if o == '-l': + if mode: + _help() + mode = 'list' + if o == '-d': + if mode: + _help() + mode = 'dump' + if o == '-V': + if mode: + _help() + mode = 'version' + if o == '-i': + mode = 'install' + if o == '-f': + force = 1 + if o == '-v': + verbose = 1 + watcher = _Watcher() + if o == '-D': + prefargs['installDir'] = a + if o == '-u': + prefargs['pimpDatabase'] = a + if not mode: + _help() + if mode == 'version': + print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__) + else: + _run(mode, verbose, force, args, prefargs, watcher) + +# Finally, try to update ourselves to a newer version. +# If the end-user updates pimp through pimp the new version +# will be called pimp_update and live in site-packages +# or somewhere similar +if __name__ != 'pimp_update': + try: + import pimp_update + except ImportError: + pass + else: + if pimp_update.PIMP_VERSION <= PIMP_VERSION: + import warnings + warnings.warn("pimp_update is version %s, not newer than pimp version %s" % + (pimp_update.PIMP_VERSION, PIMP_VERSION)) + else: + from pimp_update import * + +if __name__ == '__main__': + main() |