mirror of
https://github.com/python/cpython.git
synced 2025-09-23 17:03:23 +00:00

svn+ssh://pythondev@svn.python.org/python/branches/p3yk ................ r55326 | guido.van.rossum | 2007-05-14 15:07:35 -0700 (Mon, 14 May 2007) | 2 lines Don't use err.message, use err.args[0]. ................ r55327 | guido.van.rossum | 2007-05-14 15:11:37 -0700 (Mon, 14 May 2007) | 259 lines Merged revisions 54988-55226,55228-55323 via svnmerge from svn+ssh://pythondev@svn.python.org/python/trunk ........ r54995 | neal.norwitz | 2007-04-26 23:45:32 -0700 (Thu, 26 Apr 2007) | 3 lines This gets the test working on Solaris. It seems a little hokey to me, but the test passed on Linux and Solaris, hopefully other platforms too. ........ r55002 | georg.brandl | 2007-04-27 12:20:00 -0700 (Fri, 27 Apr 2007) | 2 lines Version fix (bug #1708710) ........ r55021 | neal.norwitz | 2007-04-29 16:53:24 -0700 (Sun, 29 Apr 2007) | 1 line There really are some tests that are problematic. ........ r55024 | kristjan.jonsson | 2007-04-30 08:17:46 -0700 (Mon, 30 Apr 2007) | 1 line Complete revamp of PCBuild8 directory. Use subdirectories for each project under the main pcbuild solution. Now make extensive use of property sheets to simplify project configuration. x64 build fully supported, and the process for building PGO version (Profiler Guided Optimization) simplified. All projects are now present, except _ssl, which needs to be reimplemented. Also, some of the projects that require external libraries need extra work to fully compile on x64. ........ r55025 | thomas.heller | 2007-04-30 08:44:17 -0700 (Mon, 30 Apr 2007) | 4 lines Make sure to call PyErr_NoMemory() in several places where PyMem_Malloc() could potentially fail. Will backport to the release25-maint branch. ........ r55027 | thomas.heller | 2007-04-30 09:04:57 -0700 (Mon, 30 Apr 2007) | 8 lines When accessing the .value attribute of a c_wchar_p instance, and the instance does not point to a valid wchar_t zero-terminated string, raise a ValueError. c_char_p does this already. The ValueError message now contains the correct pointer address. Will backport to release25-maint. ........ r55036 | georg.brandl | 2007-04-30 23:04:11 -0700 (Mon, 30 Apr 2007) | 2 lines Bug #1710295: exceptions are now new-style classes. ........ r55038 | georg.brandl | 2007-04-30 23:08:15 -0700 (Mon, 30 Apr 2007) | 2 lines Patch #1710352: add missing public functions to locale.__all__. ........ r55041 | vinay.sajip | 2007-05-01 03:20:03 -0700 (Tue, 01 May 2007) | 1 line Added new optional credentials argument to SMTPHandler.__init__, and smtp.login() is now called in SMTPHandler.emit() if credentials are specified. ........ r55042 | vinay.sajip | 2007-05-01 03:21:45 -0700 (Tue, 01 May 2007) | 1 line Added documentation for new optional credentials argument to SMTPHandler.__init__(). ........ r55070 | neal.norwitz | 2007-05-01 21:47:55 -0700 (Tue, 01 May 2007) | 3 lines Stop using PyMem_FREE while the GIL is not held. For details see: http://mail.python.org/pipermail/python-dev/2007-May/072896.html ........ r55080 | armin.rigo | 2007-05-02 12:23:31 -0700 (Wed, 02 May 2007) | 6 lines Fix for #1303614 and #1174712: - __dict__ descriptor abuse for subclasses of built-in types - subclassing from both ModuleType and another built-in types Thanks zseil for the patch. ........ r55083 | georg.brandl | 2007-05-02 13:02:29 -0700 (Wed, 02 May 2007) | 3 lines Actually raise an exception before calling ast_error_finish. Triggers an assertion otherwise. ........ r55087 | neal.norwitz | 2007-05-02 23:47:18 -0700 (Wed, 02 May 2007) | 1 line Handle a couple of uncaught errors. This should be backported ........ r55090 | neal.norwitz | 2007-05-03 00:20:57 -0700 (Thu, 03 May 2007) | 4 lines Remove dead code. This code couldn't be reached because earlier in the function there is another check for z != Py_None. ........ r55092 | thomas.heller | 2007-05-03 05:02:08 -0700 (Thu, 03 May 2007) | 1 line Fix building _ctypes.pyd for x64 / Windows. ........ r55093 | thomas.heller | 2007-05-03 05:05:20 -0700 (Thu, 03 May 2007) | 1 line Don't truncate pointers to integers (on win64 platform). ........ r55094 | walter.doerwald | 2007-05-03 08:13:55 -0700 (Thu, 03 May 2007) | 3 lines Clarify the behaviour of PyUnicode_DecodeUTF16(): A BOM is only skipped in native order mode, and only if it's the first two bytes. ........ r55101 | kristjan.jonsson | 2007-05-03 13:04:53 -0700 (Thu, 03 May 2007) | 2 lines Fix pcbuild8 after recent overhaul: Added the version resource to python26.dll. Adjust stacksize to 2Mb and made large address aware for 32 bits, and set stacksize to 3Mb for 64 bits. Todo: Set .dll optimized load addresses, and side-by-side packaging of the python26.dll. ........ r55102 | kristjan.jonsson | 2007-05-03 13:09:56 -0700 (Thu, 03 May 2007) | 1 line Fix those parts in the testsuite that assumed that sys.maxint would cause overflow on x64. Now the testsuite is well behaved on that platform. ........ r55103 | kristjan.jonsson | 2007-05-03 13:27:03 -0700 (Thu, 03 May 2007) | 11 lines Fix problems in x64 build that were discovered by the testsuite: - Reenable modules on x64 that had been disabled aeons ago for Itanium. - Cleared up confusion about compilers for 64 bit windows. There is only Itanium and x64. Added macros MS_WINI64 and MS_WINX64 for those rare cases where it matters, such as the disabling of modules above. - Set target platform (_WIN32_WINNT and WINVER) to 0x0501 (XP) for x64, and 0x0400 (NT 4.0) otherwise, which are the targeted minimum platforms. - Fixed thread_nt.h. The emulated InterlockedCompareExchange function didn?\194?\180t work on x64, probaby due to the lack of a "volatile" specifier. Anyway, win95 is no longer a target platform. - Itertools module used wrong constant to check for overflow in count() - PyInt_AsSsize_t couldn't deal with attribute error when accessing the __long__ member. - PyLong_FromSsize_t() incorrectly specified that the operand were unsigned. With these changes, the x64 passes the testsuite, for those modules present. ........ r55107 | kristjan.jonsson | 2007-05-03 17:25:08 -0700 (Thu, 03 May 2007) | 1 line Revert compiler comment to AMD64 for x64/AMD64 builds. ........ r55115 | thomas.heller | 2007-05-04 00:14:39 -0700 (Fri, 04 May 2007) | 4 lines Fix some ctypes test crashes, when running with a debug Python version on win64 by using proper argtypes and restype function attributes. ........ r55117 | thomas.heller | 2007-05-04 01:20:41 -0700 (Fri, 04 May 2007) | 4 lines On 64-bit Windows, ffi_arg must be 8 bytes long. This fixes the remaining crashes in the ctypes tests, when functions return float or double types. ........ r55120 | kristjan.jonsson | 2007-05-04 08:48:15 -0700 (Fri, 04 May 2007) | 1 line Update the pcbuild8 solution. Straightened out the _ctypes project by using a .vsproj file and a masm64.rules file to avoid redundancy ........ r55121 | kristjan.jonsson | 2007-05-04 10:28:06 -0700 (Fri, 04 May 2007) | 1 line Minor fix of PCBuild8/_ctypes vcproj, moving include dir into the .vsprops file. ........ r55129 | thomas.heller | 2007-05-04 12:54:22 -0700 (Fri, 04 May 2007) | 3 lines Do not truncate 64-bit pointers to 32-bit integers. Fixes SF #1703286, will backport to release25-maint. ........ r55131 | thomas.heller | 2007-05-04 12:56:32 -0700 (Fri, 04 May 2007) | 1 line Oops, these tests do not run on Windows CE. ........ r55140 | brett.cannon | 2007-05-04 18:34:02 -0700 (Fri, 04 May 2007) | 2 lines Deprecate BaseException.message as per PEP 352. ........ r55154 | georg.brandl | 2007-05-05 11:55:37 -0700 (Sat, 05 May 2007) | 2 lines Bug #1713535: typo in logging example. ........ r55158 | vinay.sajip | 2007-05-06 10:53:37 -0700 (Sun, 06 May 2007) | 1 line Updates of recent changes to logging. ........ r55165 | neal.norwitz | 2007-05-07 00:02:26 -0700 (Mon, 07 May 2007) | 1 line Verify changes to the trunk go to the normal checkins list ........ r55169 | kristjan.jonsson | 2007-05-07 09:46:54 -0700 (Mon, 07 May 2007) | 1 line As per Armin Rigo's suggestion, remove special handing from intobject.c to deal with the peculiarities of classobject's implementation of the number protocol. The nb_long method of classobject now falls back to nb_int if there is no __long__ attribute present. ........ r55197 | collin.winter | 2007-05-08 21:14:36 -0700 (Tue, 08 May 2007) | 9 lines Fix a bug in test.test_support.open_urlresource(). If the call to requires() doesn't precede the filesystem check, we get the following situation: 1. ./python Lib/test/regrtest.py test_foo # test needs urlfetch, not enabled, so skipped 2. ./python Lib/test/regrtest.py -u urlfetch test_foo # test runs 3. ./python Lib/test/regrtest.py test_foo # test runs (!) By moving the call to requires() *before* the filesystem check, the fact that fetched files are cached on the local disk becomes an implementation detail, rather than a semantics-changing point of note. ........ r55198 | neal.norwitz | 2007-05-08 23:43:15 -0700 (Tue, 08 May 2007) | 1 line Add markup for True/False. Will backport ........ r55205 | walter.doerwald | 2007-05-09 11:10:47 -0700 (Wed, 09 May 2007) | 4 lines Backport checkin: Fix a segfault when b"" was passed to b2a_qp() -- it was using strchr() instead of memchr(). ........ r55241 | neal.norwitz | 2007-05-10 22:55:15 -0700 (Thu, 10 May 2007) | 6 lines Don't ever report a failure when the sum of the reference count differences are zero. This should help reduce the false positives. The message about references leaking is maintained to provide as much info as possible rather than simply suppressing the message at the source. ........ r55242 | neal.norwitz | 2007-05-10 23:23:01 -0700 (Thu, 10 May 2007) | 1 line Fix typo in docstring (the module is popen2, not 3). ........ r55244 | neal.norwitz | 2007-05-10 23:56:52 -0700 (Thu, 10 May 2007) | 1 line Remove trailing whitespace in docstring ........ r55245 | neal.norwitz | 2007-05-10 23:57:33 -0700 (Thu, 10 May 2007) | 1 line Deprecate os.popen* and popen2 module in favor of the subprocess module. ........ r55247 | neal.norwitz | 2007-05-11 00:13:30 -0700 (Fri, 11 May 2007) | 1 line Deprecate os.popen* and popen2 module in favor of the subprocess module. (forgot the doc) ........ r55253 | georg.brandl | 2007-05-11 02:41:37 -0700 (Fri, 11 May 2007) | 3 lines Remove an XXX that is unnecessary. ........ r55258 | georg.brandl | 2007-05-11 04:04:26 -0700 (Fri, 11 May 2007) | 2 lines Patch #1714700: clarify os.linesep vs. tfiles opened in text mode. (backport) ........ r55259 | georg.brandl | 2007-05-11 04:43:56 -0700 (Fri, 11 May 2007) | 2 lines Update DDJ link. ........ r55273 | raymond.hettinger | 2007-05-11 10:59:59 -0700 (Fri, 11 May 2007) | 1 line Better tests for posixpath.commonprefix ........ r55287 | georg.brandl | 2007-05-12 14:06:41 -0700 (Sat, 12 May 2007) | 2 lines Bug #1046945: document SWIG options of distutils. ........ r55290 | georg.brandl | 2007-05-13 01:04:07 -0700 (Sun, 13 May 2007) | 2 lines Add bz2 to content encodings. ........ r55297 | neal.norwitz | 2007-05-13 13:45:05 -0700 (Sun, 13 May 2007) | 3 lines Remove Amoeba doc which was removed in version 1.0! according to Misc/HISTORY. Hopefully Guido won't shed a tear. :-) ........ r55298 | neal.norwitz | 2007-05-13 13:54:19 -0700 (Sun, 13 May 2007) | 1 line Remove references to stdwin which was removed long ago. ........ r55299 | neal.norwitz | 2007-05-13 14:13:42 -0700 (Sun, 13 May 2007) | 3 lines Remove support for freebsd[23] which haven't been released since 2000 or earlier. http://www.freebsd.org/releases/index.html ........ r55320 | raymond.hettinger | 2007-05-14 13:52:31 -0700 (Mon, 14 May 2007) | 1 line Small speedup. ........ ................
1178 lines
42 KiB
Python
1178 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()
|