mirror of https://github.com/python/cpython
1179 lines
42 KiB
Python
1179 lines
42 KiB
Python
"""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.
|
|
"""
|
|
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 as 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 = int(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, namespace)
|
|
except ImportError as arg:
|
|
return "no", str(arg)
|
|
except _scriptExc_NotInstalled as arg:
|
|
return "no", str(arg)
|
|
except _scriptExc_OldInstalled as arg:
|
|
return "old", str(arg)
|
|
except _scriptExc_BadInstalled as 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()
|