diff options
Diffstat (limited to 'Lib/plat-mac/pimp.py')
-rw-r--r-- | Lib/plat-mac/pimp.py | 260 |
1 files changed, 129 insertions, 131 deletions
diff --git a/Lib/plat-mac/pimp.py b/Lib/plat-mac/pimp.py index 39bc693..67fab76 100644 --- a/Lib/plat-mac/pimp.py +++ b/Lib/plat-mac/pimp.py @@ -1,6 +1,6 @@ """Package Install Manager for Python. -This is currently a MacOSX-only strawman implementation. +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 @@ -27,7 +27,7 @@ import tempfile import shutil import time -__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main", +__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main", "PIMP_VERSION", "main"] _scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled" @@ -52,12 +52,12 @@ def getDefaultDatabase(experimental=False): status = "exp" else: status = "prod" - + major, minor, micro, state, extra = sys.version_info pyvers = '%d.%d' % (major, minor) if 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 @@ -94,7 +94,7 @@ def getDefaultDatabase(experimental=False): 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) @@ -112,22 +112,22 @@ def _cmd(output, dir, *cmditems): 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): @@ -138,7 +138,7 @@ class PimpCurlDownloader(PimpDownloader): url) self.update("Downloading %s: finished" % url) return (not exitstatus) - + class PimpUrllibDownloader(PimpDownloader): def download(self, url, filename, output=None): @@ -150,13 +150,13 @@ class PimpUrllibDownloader(PimpDownloader): 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: + if len(data) == 0: #this is our exit condition break output.write(data) @@ -171,12 +171,12 @@ class PimpUrllibDownloader(PimpDownloader): 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=[], @@ -187,30 +187,30 @@ class PimpUnpacker: 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() @@ -253,7 +253,7 @@ class PimpTarUnpacker(PimpUnpacker): names = package.filterExpectedSkips(names) if names: return "Not all files were unpacked: %s" % " ".join(names) - + ARCHIVE_FORMATS = [ (".tar.Z", PimpTarUnpacker, None), (".taz", PimpTarUnpacker, None), @@ -266,8 +266,8 @@ ARCHIVE_FORMATS = [ class PimpPreferences: """Container for per-user preferences, such as the database to use and where to install packages.""" - - def __init__(self, + + def __init__(self, flavorOrder=None, downloadDir=None, buildDir=None, @@ -287,10 +287,10 @@ class PimpPreferences: 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. @@ -303,14 +303,14 @@ class PimpPreferences: 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): @@ -337,7 +337,7 @@ class PimpPreferences: 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.""" @@ -348,13 +348,13 @@ class PimpPreferences: 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 @@ -363,23 +363,23 @@ class PimpDatabase: 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) @@ -396,7 +396,7 @@ class PimpDatabase: 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" + 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() @@ -405,12 +405,12 @@ class PimpDatabase: others = plistdata.get('Include', []) for url in others: self.appendURL(url, included=1) - + def _appendPackages(self, packages): """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) flavor = p.get('Flavor') @@ -421,27 +421,27 @@ class PimpDatabase: 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()) @@ -453,15 +453,15 @@ class PimpDatabase: } 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] == ')': @@ -491,7 +491,7 @@ class PimpDatabase: if not found or found < p: found = p return found - + ALLOWED_KEYS = [ "Name", "Version", @@ -511,7 +511,7 @@ ALLOWED_KEYS = [ class PimpPackage: """Class representing a single package.""" - + def __init__(self, db, plistdata): self._db = db name = plistdata["Name"] @@ -519,10 +519,10 @@ class PimpPackage: 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') @@ -531,13 +531,13 @@ class PimpPackage: 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 self._dict.has_key('Version'): rv = rv + '-%s' % self._dict['Version'] @@ -547,14 +547,14 @@ class PimpPackage: # 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(): @@ -562,15 +562,15 @@ class PimpPackage: 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, @@ -602,16 +602,16 @@ class PimpPackage: 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 @@ -619,7 +619,7 @@ class PimpPackage: status, _ = self.installed() if status == "yes": return [] - return [(None, + return [(None, "%s: This package cannot be installed automatically (no Download-URL field)" % self.fullname())] if self.systemwideOnly() and self._db.preferences.isUserInstall(): @@ -645,28 +645,28 @@ class PimpPackage: 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) + 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, + downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir, watcher=self._db.preferences.watcher) if not downloader.download(self._dict['Download-URL'], self.archiveFilename, output): @@ -675,10 +675,10 @@ class PimpPackage: 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'): @@ -687,10 +687,10 @@ class PimpPackage: data = open(self.archiveFilename, 'rb').read() checksum = md5.new(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: @@ -698,43 +698,43 @@ class PimpPackage: else: return "unknown extension for archive file: %s" % filename self.basename = filename[:-len(ext)] - unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir, + 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: @@ -757,7 +757,7 @@ class PimpPackage: line = os.path.join(self._db.preferences.installDir, line) line = os.path.realpath(line) if not line in sys.path: - sys.path.append(line) + sys.path.append(line) def filterExpectedSkips(self, names): """Return a list that contains only unpexpected skips""" @@ -780,21 +780,21 @@ 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 self._dict.has_key('Install-command'): return "%s: Binary package cannot have Install-command" % self.fullname() - + if self._dict.has_key('Pre-install-command'): 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() # Install by unpacking @@ -805,7 +805,7 @@ class PimpPackage_binary(PimpPackage): 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: @@ -815,22 +815,22 @@ class PimpPackage_binary(PimpPackage): 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 self._dict.has_key('Post-install-command'): if _cmd(output, self._buildDirname, 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): @@ -844,15 +844,15 @@ class PimpPackage_source(PimpPackage): 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 self._dict.has_key('Pre-install-command'): 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: @@ -882,45 +882,45 @@ class PimpPackage_source(PimpPackage): rv = None shutil.rmtree(unwanted_install_dir) return rv - + self.afterInstall() - + if self._dict.has_key('Post-install-command'): 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 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 + return if package in self._todo or package in self._curtodo: return self._curtodo.insert(0, package) @@ -932,17 +932,17 @@ class PimpInstaller: 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) @@ -950,10 +950,10 @@ class PimpInstaller: 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: @@ -961,12 +961,12 @@ class PimpInstaller: 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) @@ -975,7 +975,7 @@ def _run(mode, verbose, force, args, prefargs, watcher): sys.stdout.write(rv) db = PimpDatabase(prefs) db.appendURL(prefs.pimpDatabase) - + if mode == 'dump': db.dump(sys.stdout) elif mode =='list': @@ -1050,7 +1050,7 @@ def _run(mode, verbose, force, args, prefargs, watcher): def main(): """Minimal commandline tool to drive pimp.""" - + import getopt def _help(): print "Usage: pimp [options] -s [package ...] List installed status" @@ -1065,12 +1065,12 @@ def main(): 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: @@ -1133,8 +1133,6 @@ if __name__ != 'pimp_update': (pimp_update.PIMP_VERSION, PIMP_VERSION)) else: from pimp_update import * - + if __name__ == '__main__': main() - - |