blob: 131d5b0dca3013bfb7f0f9cf035b380457eb22e6 [file] [log] [blame]
Jack Jansen6a600ab2003-02-10 15:55:51 +00001"""Package Install Manager for Python.
2
3This is currently a MacOSX-only strawman implementation.
Jack Jansenc5991b02003-06-29 00:09:18 +00004Despite other rumours the name stands for "Packman IMPlementation".
Jack Jansen6a600ab2003-02-10 15:55:51 +00005
6Tools to allow easy installation of packages. The idea is that there is
7an online XML database per (platform, python-version) containing packages
8known to work with that combination. This module contains tools for getting
9and parsing the database, testing whether packages are installed, computing
10dependencies and installing packages.
11
12There is a minimal main program that works as a command line tool, but the
13intention is that the end user will use this through a GUI.
14"""
Jack Jansen95839b82003-02-09 23:10:20 +000015import sys
16import os
Jack Jansen450bd872003-03-17 10:54:41 +000017import popen2
Jack Jansen95839b82003-02-09 23:10:20 +000018import urllib
Jack Jansen47e59872003-03-11 14:37:19 +000019import urllib2
Jack Jansen95839b82003-02-09 23:10:20 +000020import urlparse
21import plistlib
22import distutils.util
Jack Jansene71b9f82003-02-12 16:37:00 +000023import distutils.sysconfig
Jack Jansenc4b217d2003-02-10 13:38:44 +000024import md5
Jack Jansen6fde1ce2003-04-15 14:43:05 +000025import tarfile
26import tempfile
27import shutil
Jack Jansen95839b82003-02-09 23:10:20 +000028
Jack Jansenb789a062003-05-28 18:56:30 +000029__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main",
30 "PIMP_VERSION", "main"]
Jack Jansen6a600ab2003-02-10 15:55:51 +000031
Jack Jansen95839b82003-02-09 23:10:20 +000032_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
33_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
34_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
35
36NO_EXECUTE=0
37
Jack Jansenafd63b92004-02-28 22:34:02 +000038PIMP_VERSION="0.4"
Jack Jansene7b33db2003-02-11 22:40:59 +000039
Jack Jansen0dacac42003-02-14 14:11:59 +000040# Flavors:
41# source: setup-based package
42# binary: tar (or other) archive created with setup.py bdist.
Jack Jansen95839b82003-02-09 23:10:20 +000043DEFAULT_FLAVORORDER=['source', 'binary']
44DEFAULT_DOWNLOADDIR='/tmp'
45DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000046DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansenafd63b92004-02-28 22:34:02 +000047DEFAULT_PIMPDATABASE_FMT="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist"
Jack Jansen95839b82003-02-09 23:10:20 +000048
Jack Jansen192bd962004-02-28 23:18:43 +000049def getDefaultDatabase(experimental=False):
50 if experimental:
51 status = "exp"
52 else:
53 status = "prod"
54
55 major, minor, micro, state, extra = sys.version_info
56 pyvers = '%d.%d' % (major, minor)
57 if state != 'final':
58 pyvers = pyvers + '%s%d' % (state, extra)
59
60 longplatform = distutils.util.get_platform()
61 osname, release, machine = longplatform.split('-')
62 # For some platforms we may want to differentiate between
63 # installation types
64 if osname == 'darwin':
65 if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'):
66 osname = 'darwin_apple'
67 elif sys.prefix.startswith('/Library/Frameworks/Python.framework'):
68 osname = 'darwin_macpython'
69 # Otherwise we don't know...
70 # Now we try various URLs by playing with the release string.
71 # We remove numbers off the end until we find a match.
72 rel = release
73 while True:
74 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine)
75 try:
76 urllib2.urlopen(url)
77 except urllib2.HTTPError, arg:
78 pass
79 else:
80 break
81 if not rel:
82 # We're out of version numbers to try. Use the
83 # full release number, this will give a reasonable
84 # error message later
85 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine)
86 break
87 idx = rel.rfind('.')
88 if idx < 0:
89 rel = ''
90 else:
91 rel = rel[:idx]
92 return url
93
Jack Jansen6fde1ce2003-04-15 14:43:05 +000094def _cmd(output, dir, *cmditems):
95 """Internal routine to run a shell command in a given directory."""
96
97 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
98 if output:
99 output.write("+ %s\n" % cmd)
100 if NO_EXECUTE:
101 return 0
102 child = popen2.Popen4(cmd)
103 child.tochild.close()
104 while 1:
105 line = child.fromchild.readline()
106 if not line:
107 break
108 if output:
109 output.write(line)
110 return child.wait()
111
112class PimpUnpacker:
113 """Abstract base class - Unpacker for archives"""
114
115 _can_rename = False
116
117 def __init__(self, argument,
118 dir="",
119 renames=[]):
120 self.argument = argument
121 if renames and not self._can_rename:
122 raise RuntimeError, "This unpacker cannot rename files"
123 self._dir = dir
124 self._renames = renames
125
Jack Jansen5da131b2003-06-01 20:57:12 +0000126 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000127 return None
128
129class PimpCommandUnpacker(PimpUnpacker):
130 """Unpack archives by calling a Unix utility"""
131
132 _can_rename = False
133
Jack Jansen5da131b2003-06-01 20:57:12 +0000134 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000135 cmd = self.argument % archive
136 if _cmd(output, self._dir, cmd):
137 return "unpack command failed"
138
139class PimpTarUnpacker(PimpUnpacker):
140 """Unpack tarfiles using the builtin tarfile module"""
141
142 _can_rename = True
143
Jack Jansen5da131b2003-06-01 20:57:12 +0000144 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000145 tf = tarfile.open(archive, "r")
146 members = tf.getmembers()
147 skip = []
148 if self._renames:
149 for member in members:
150 for oldprefix, newprefix in self._renames:
151 if oldprefix[:len(self._dir)] == self._dir:
152 oldprefix2 = oldprefix[len(self._dir):]
153 else:
154 oldprefix2 = None
155 if member.name[:len(oldprefix)] == oldprefix:
156 if newprefix is None:
157 skip.append(member)
158 #print 'SKIP', member.name
159 else:
160 member.name = newprefix + member.name[len(oldprefix):]
161 print ' ', member.name
162 break
163 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
164 if newprefix is None:
165 skip.append(member)
166 #print 'SKIP', member.name
167 else:
168 member.name = newprefix + member.name[len(oldprefix2):]
169 #print ' ', member.name
170 break
171 else:
172 skip.append(member)
173 #print '????', member.name
174 for member in members:
175 if member in skip:
176 continue
177 tf.extract(member, self._dir)
178 if skip:
179 names = [member.name for member in skip if member.name[-1] != '/']
Jack Jansen5da131b2003-06-01 20:57:12 +0000180 if package:
181 names = package.filterExpectedSkips(names)
Jack Jansen6432f782003-04-22 13:56:19 +0000182 if names:
Jack Jansen705553a2003-05-06 12:44:00 +0000183 return "Not all files were unpacked: %s" % " ".join(names)
Jack Jansen5da131b2003-06-01 20:57:12 +0000184
Jack Jansen95839b82003-02-09 23:10:20 +0000185ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000186 (".tar.Z", PimpTarUnpacker, None),
187 (".taz", PimpTarUnpacker, None),
188 (".tar.gz", PimpTarUnpacker, None),
189 (".tgz", PimpTarUnpacker, None),
190 (".tar.bz", PimpTarUnpacker, None),
191 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000192]
193
194class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000195 """Container for per-user preferences, such as the database to use
196 and where to install packages."""
197
198 def __init__(self,
199 flavorOrder=None,
200 downloadDir=None,
201 buildDir=None,
202 installDir=None,
203 pimpDatabase=None):
204 if not flavorOrder:
205 flavorOrder = DEFAULT_FLAVORORDER
206 if not downloadDir:
207 downloadDir = DEFAULT_DOWNLOADDIR
208 if not buildDir:
209 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000210 if not pimpDatabase:
Jack Jansen192bd962004-02-28 23:18:43 +0000211 pimpDatabase = getDefaultDatabase()
Jack Jansen20fa6752003-04-16 12:15:34 +0000212 self.setInstallDir(installDir)
213 self.flavorOrder = flavorOrder
214 self.downloadDir = downloadDir
215 self.buildDir = buildDir
216 self.pimpDatabase = pimpDatabase
217
218 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000219 if installDir:
220 # Installing to non-standard location.
221 self.installLocations = [
222 ('--install-lib', installDir),
223 ('--install-headers', None),
224 ('--install-scripts', None),
225 ('--install-data', None)]
226 else:
227 installDir = DEFAULT_INSTALLDIR
228 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000229 self.installDir = installDir
Jack Jansen5da131b2003-06-01 20:57:12 +0000230
231 def isUserInstall(self):
232 return self.installDir != DEFAULT_INSTALLDIR
Jack Jansen20fa6752003-04-16 12:15:34 +0000233
Jack Jansen0ae32202003-04-09 13:25:43 +0000234 def check(self):
235 """Check that the preferences make sense: directories exist and are
236 writable, the install directory is on sys.path, etc."""
237
238 rv = ""
239 RWX_OK = os.R_OK|os.W_OK|os.X_OK
240 if not os.path.exists(self.downloadDir):
241 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
242 elif not os.access(self.downloadDir, RWX_OK):
243 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
244 if not os.path.exists(self.buildDir):
245 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
246 elif not os.access(self.buildDir, RWX_OK):
247 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
248 if not os.path.exists(self.installDir):
249 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
250 elif not os.access(self.installDir, RWX_OK):
251 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
252 else:
253 installDir = os.path.realpath(self.installDir)
254 for p in sys.path:
255 try:
256 realpath = os.path.realpath(p)
257 except:
258 pass
259 if installDir == realpath:
260 break
261 else:
262 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000263 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000264
265 def compareFlavors(self, left, right):
266 """Compare two flavor strings. This is part of your preferences
267 because whether the user prefers installing from source or binary is."""
268 if left in self.flavorOrder:
269 if right in self.flavorOrder:
270 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
271 return -1
272 if right in self.flavorOrder:
273 return 1
274 return cmp(left, right)
275
Jack Jansen95839b82003-02-09 23:10:20 +0000276class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000277 """Class representing a pimp database. It can actually contain
278 information from multiple databases through inclusion, but the
279 toplevel database is considered the master, as its maintainer is
280 "responsible" for the contents."""
281
282 def __init__(self, prefs):
283 self._packages = []
284 self.preferences = prefs
285 self._urllist = []
286 self._version = ""
287 self._maintainer = ""
288 self._description = ""
289
290 def close(self):
291 """Clean up"""
292 self._packages = []
293 self.preferences = None
294
295 def appendURL(self, url, included=0):
296 """Append packages from the database with the given URL.
297 Only the first database should specify included=0, so the
298 global information (maintainer, description) get stored."""
299
300 if url in self._urllist:
301 return
302 self._urllist.append(url)
303 fp = urllib2.urlopen(url).fp
304 dict = plistlib.Plist.fromFile(fp)
305 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000306 if included:
307 version = dict.get('Version')
308 if version and version > self._version:
309 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
310 (url, version))
311 else:
312 self._version = dict.get('Version')
313 if not self._version:
314 sys.stderr.write("Warning: database has no Version information\n")
315 elif self._version > PIMP_VERSION:
316 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000317 % (self._version, PIMP_VERSION))
318 self._maintainer = dict.get('Maintainer', '')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000319 self._description = dict.get('Description', '').strip()
Jack Jansen0ae32202003-04-09 13:25:43 +0000320 self._appendPackages(dict['Packages'])
321 others = dict.get('Include', [])
322 for url in others:
323 self.appendURL(url, included=1)
324
325 def _appendPackages(self, packages):
326 """Given a list of dictionaries containing package
327 descriptions create the PimpPackage objects and append them
328 to our internal storage."""
329
330 for p in packages:
331 p = dict(p)
332 flavor = p.get('Flavor')
333 if flavor == 'source':
334 pkg = PimpPackage_source(self, p)
335 elif flavor == 'binary':
336 pkg = PimpPackage_binary(self, p)
337 else:
338 pkg = PimpPackage(self, dict(p))
339 self._packages.append(pkg)
340
341 def list(self):
342 """Return a list of all PimpPackage objects in the database."""
343
344 return self._packages
345
346 def listnames(self):
347 """Return a list of names of all packages in the database."""
348
349 rv = []
350 for pkg in self._packages:
351 rv.append(pkg.fullname())
352 rv.sort()
353 return rv
354
355 def dump(self, pathOrFile):
356 """Dump the contents of the database to an XML .plist file.
357
358 The file can be passed as either a file object or a pathname.
359 All data, including included databases, is dumped."""
360
361 packages = []
362 for pkg in self._packages:
363 packages.append(pkg.dump())
364 dict = {
365 'Version': self._version,
366 'Maintainer': self._maintainer,
367 'Description': self._description,
368 'Packages': packages
369 }
370 plist = plistlib.Plist(**dict)
371 plist.write(pathOrFile)
372
373 def find(self, ident):
374 """Find a package. The package can be specified by name
375 or as a dictionary with name, version and flavor entries.
376
377 Only name is obligatory. If there are multiple matches the
378 best one (higher version number, flavors ordered according to
379 users' preference) is returned."""
380
381 if type(ident) == str:
382 # Remove ( and ) for pseudo-packages
383 if ident[0] == '(' and ident[-1] == ')':
384 ident = ident[1:-1]
385 # Split into name-version-flavor
386 fields = ident.split('-')
387 if len(fields) < 1 or len(fields) > 3:
388 return None
389 name = fields[0]
390 if len(fields) > 1:
391 version = fields[1]
392 else:
393 version = None
394 if len(fields) > 2:
395 flavor = fields[2]
396 else:
397 flavor = None
398 else:
399 name = ident['Name']
400 version = ident.get('Version')
401 flavor = ident.get('Flavor')
402 found = None
403 for p in self._packages:
404 if name == p.name() and \
405 (not version or version == p.version()) and \
406 (not flavor or flavor == p.flavor()):
407 if not found or found < p:
408 found = p
409 return found
410
Jack Jansene7b33db2003-02-11 22:40:59 +0000411ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000412 "Name",
413 "Version",
414 "Flavor",
415 "Description",
416 "Home-page",
417 "Download-URL",
418 "Install-test",
419 "Install-command",
420 "Pre-install-command",
421 "Post-install-command",
422 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000423 "MD5Sum",
424 "User-install-skips",
425 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000426]
427
Jack Jansen95839b82003-02-09 23:10:20 +0000428class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000429 """Class representing a single package."""
430
431 def __init__(self, db, dict):
432 self._db = db
433 name = dict["Name"]
434 for k in dict.keys():
435 if not k in ALLOWED_KEYS:
436 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
437 self._dict = dict
438
439 def __getitem__(self, key):
440 return self._dict[key]
441
442 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000443 def version(self): return self._dict.get('Version')
444 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000445 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000446 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000447 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000448 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000449 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Jack Jansen0ae32202003-04-09 13:25:43 +0000450
451 def fullname(self):
452 """Return the full name "name-version-flavor" of a package.
453
454 If the package is a pseudo-package, something that cannot be
455 installed through pimp, return the name in (parentheses)."""
456
457 rv = self._dict['Name']
458 if self._dict.has_key('Version'):
459 rv = rv + '-%s' % self._dict['Version']
460 if self._dict.has_key('Flavor'):
461 rv = rv + '-%s' % self._dict['Flavor']
462 if not self._dict.get('Download-URL'):
463 # Pseudo-package, show in parentheses
464 rv = '(%s)' % rv
465 return rv
466
467 def dump(self):
468 """Return a dict object containing the information on the package."""
469 return self._dict
470
471 def __cmp__(self, other):
472 """Compare two packages, where the "better" package sorts lower."""
473
474 if not isinstance(other, PimpPackage):
475 return cmp(id(self), id(other))
476 if self.name() != other.name():
477 return cmp(self.name(), other.name())
478 if self.version() != other.version():
479 return -cmp(self.version(), other.version())
480 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
481
482 def installed(self):
483 """Test wheter the package is installed.
484
485 Returns two values: a status indicator which is one of
486 "yes", "no", "old" (an older version is installed) or "bad"
487 (something went wrong during the install test) and a human
488 readable string which may contain more details."""
489
490 namespace = {
491 "NotInstalled": _scriptExc_NotInstalled,
492 "OldInstalled": _scriptExc_OldInstalled,
493 "BadInstalled": _scriptExc_BadInstalled,
494 "os": os,
495 "sys": sys,
496 }
497 installTest = self._dict['Install-test'].strip() + '\n'
498 try:
499 exec installTest in namespace
500 except ImportError, arg:
501 return "no", str(arg)
502 except _scriptExc_NotInstalled, arg:
503 return "no", str(arg)
504 except _scriptExc_OldInstalled, arg:
505 return "old", str(arg)
506 except _scriptExc_BadInstalled, arg:
507 return "bad", str(arg)
508 except:
509 sys.stderr.write("-------------------------------------\n")
510 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
511 sys.stderr.write("---- source:\n")
512 sys.stderr.write(installTest)
513 sys.stderr.write("---- exception:\n")
514 import traceback
515 traceback.print_exc(file=sys.stderr)
516 if self._db._maintainer:
517 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
518 sys.stderr.write("-------------------------------------\n")
519 return "bad", "Package install test got exception"
520 return "yes", ""
521
522 def prerequisites(self):
523 """Return a list of prerequisites for this package.
524
525 The list contains 2-tuples, of which the first item is either
526 a PimpPackage object or None, and the second is a descriptive
527 string. The first item can be None if this package depends on
528 something that isn't pimp-installable, in which case the descriptive
529 string should tell the user what to do."""
530
531 rv = []
532 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000533 # For pseudo-packages that are already installed we don't
534 # return an error message
535 status, _ = self.installed()
536 if status == "yes":
537 return []
Jack Jansen0ae32202003-04-09 13:25:43 +0000538 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000539 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000540 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000541 if self.systemwideOnly() and self._db.preferences.isUserInstall():
542 return [(None,
543 "%s: This package can only be installed system-wide" %
544 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000545 if not self._dict.get('Prerequisites'):
546 return []
547 for item in self._dict['Prerequisites']:
548 if type(item) == str:
549 pkg = None
550 descr = str(item)
551 else:
552 name = item['Name']
553 if item.has_key('Version'):
554 name = name + '-' + item['Version']
555 if item.has_key('Flavor'):
556 name = name + '-' + item['Flavor']
557 pkg = self._db.find(name)
558 if not pkg:
559 descr = "Requires unknown %s"%name
560 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000561 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000562 rv.append((pkg, descr))
563 return rv
564
Jack Jansen0ae32202003-04-09 13:25:43 +0000565
566 def downloadPackageOnly(self, output=None):
567 """Download a single package, if needed.
568
569 An MD5 signature is used to determine whether download is needed,
570 and to test that we actually downloaded what we expected.
571 If output is given it is a file-like object that will receive a log
572 of what happens.
573
574 If anything unforeseen happened the method returns an error message
575 string.
576 """
577
578 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
579 path = urllib.url2pathname(path)
580 filename = os.path.split(path)[1]
581 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
582 if not self._archiveOK():
583 if scheme == 'manual':
584 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000585 if _cmd(output, self._db.preferences.downloadDir,
Jack Jansen0ae32202003-04-09 13:25:43 +0000586 "curl",
587 "--output", self.archiveFilename,
588 self._dict['Download-URL']):
589 return "download command failed"
590 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
591 return "archive not found after download"
592 if not self._archiveOK():
593 return "archive does not have correct MD5 checksum"
594
595 def _archiveOK(self):
596 """Test an archive. It should exist and the MD5 checksum should be correct."""
597
598 if not os.path.exists(self.archiveFilename):
599 return 0
600 if not self._dict.get('MD5Sum'):
601 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
602 return 1
603 data = open(self.archiveFilename, 'rb').read()
604 checksum = md5.new(data).hexdigest()
605 return checksum == self._dict['MD5Sum']
606
607 def unpackPackageOnly(self, output=None):
608 """Unpack a downloaded package archive."""
609
610 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000611 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000612 if filename[-len(ext):] == ext:
613 break
614 else:
615 return "unknown extension for archive file: %s" % filename
616 self.basename = filename[:-len(ext)]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000617 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir)
618 rv = unpacker.unpack(self.archiveFilename, output=output)
619 if rv:
620 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000621
622 def installPackageOnly(self, output=None):
623 """Default install method, to be overridden by subclasses"""
624 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
625 % (self.fullname(), self._dict.get(flavor, ""))
626
627 def installSinglePackage(self, output=None):
628 """Download, unpack and install a single package.
629
630 If output is given it should be a file-like object and it
631 will receive a log of what happened."""
632
Jack Jansen749f4812003-07-21 20:47:11 +0000633 if not self._dict.get('Download-URL'):
634 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000635 msg = self.downloadPackageOnly(output)
636 if msg:
637 return "%s: download: %s" % (self.fullname(), msg)
638
639 msg = self.unpackPackageOnly(output)
640 if msg:
641 return "%s: unpack: %s" % (self.fullname(), msg)
642
643 return self.installPackageOnly(output)
644
645 def beforeInstall(self):
646 """Bookkeeping before installation: remember what we have in site-packages"""
647 self._old_contents = os.listdir(self._db.preferences.installDir)
648
649 def afterInstall(self):
650 """Bookkeeping after installation: interpret any new .pth files that have
651 appeared"""
652
653 new_contents = os.listdir(self._db.preferences.installDir)
654 for fn in new_contents:
655 if fn in self._old_contents:
656 continue
657 if fn[-4:] != '.pth':
658 continue
659 fullname = os.path.join(self._db.preferences.installDir, fn)
660 f = open(fullname)
661 for line in f.readlines():
662 if not line:
663 continue
664 if line[0] == '#':
665 continue
666 if line[:6] == 'import':
667 exec line
668 continue
669 if line[-1] == '\n':
670 line = line[:-1]
671 if not os.path.isabs(line):
672 line = os.path.join(self._db.preferences.installDir, line)
673 line = os.path.realpath(line)
674 if not line in sys.path:
675 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000676
Jack Jansen5da131b2003-06-01 20:57:12 +0000677 def filterExpectedSkips(self, names):
678 """Return a list that contains only unpexpected skips"""
679 if not self._db.preferences.isUserInstall():
680 return names
681 expected_skips = self._dict.get('User-install-skips')
682 if not expected_skips:
683 return names
684 newnames = []
685 for name in names:
686 for skip in expected_skips:
687 if name[:len(skip)] == skip:
688 break
689 else:
690 newnames.append(name)
691 return newnames
692
Jack Jansen0dacac42003-02-14 14:11:59 +0000693class PimpPackage_binary(PimpPackage):
694
Jack Jansen0ae32202003-04-09 13:25:43 +0000695 def unpackPackageOnly(self, output=None):
696 """We don't unpack binary packages until installing"""
697 pass
698
699 def installPackageOnly(self, output=None):
700 """Install a single source package.
701
702 If output is given it should be a file-like object and it
703 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000704
Jack Jansen0ae32202003-04-09 13:25:43 +0000705 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000706 return "%s: Binary package cannot have Install-command" % self.fullname()
707
708 if self._dict.has_key('Pre-install-command'):
709 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
710 return "pre-install %s: running \"%s\" failed" % \
711 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000712
713 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000714
Jack Jansen0ae32202003-04-09 13:25:43 +0000715 # Install by unpacking
716 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000717 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000718 if filename[-len(ext):] == ext:
719 break
720 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000721 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
722 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000723
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000724 install_renames = []
725 for k, newloc in self._db.preferences.installLocations:
726 if not newloc:
727 continue
728 if k == "--install-lib":
729 oldloc = DEFAULT_INSTALLDIR
730 else:
731 return "%s: Don't know installLocation %s" % (self.fullname(), k)
732 install_renames.append((oldloc, newloc))
733
734 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000735 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000736 if rv:
737 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000738
739 self.afterInstall()
740
741 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000742 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
743 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000744 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000745
Jack Jansen0ae32202003-04-09 13:25:43 +0000746 return None
747
748
Jack Jansen0dacac42003-02-14 14:11:59 +0000749class PimpPackage_source(PimpPackage):
750
Jack Jansen0ae32202003-04-09 13:25:43 +0000751 def unpackPackageOnly(self, output=None):
752 """Unpack a source package and check that setup.py exists"""
753 PimpPackage.unpackPackageOnly(self, output)
754 # Test that a setup script has been create
755 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
756 setupname = os.path.join(self._buildDirname, "setup.py")
757 if not os.path.exists(setupname) and not NO_EXECUTE:
758 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000759
Jack Jansen0ae32202003-04-09 13:25:43 +0000760 def installPackageOnly(self, output=None):
761 """Install a single source package.
762
763 If output is given it should be a file-like object and it
764 will receive a log of what happened."""
765
766 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000767 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000768 return "pre-install %s: running \"%s\" failed" % \
769 (self.fullname(), self._dict['Pre-install-command'])
770
771 self.beforeInstall()
772 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000773 if installcmd and self._install_renames:
774 return "Package has install-command and can only be installed to standard location"
775 # This is the "bit-bucket" for installations: everything we don't
776 # want. After installation we check that it is actually empty
777 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000778 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000779 extra_args = ""
780 for k, v in self._db.preferences.installLocations:
781 if not v:
782 # We don't want these files installed. Send them
783 # to the bit-bucket.
784 if not unwanted_install_dir:
785 unwanted_install_dir = tempfile.mkdtemp()
786 v = unwanted_install_dir
787 extra_args = extra_args + " %s \"%s\"" % (k, v)
788 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
789 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000790 return "install %s: running \"%s\" failed" % \
791 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000792 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
793 unwanted_files = os.listdir(unwanted_install_dir)
794 if unwanted_files:
795 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
796 else:
797 rv = None
798 shutil.rmtree(unwanted_install_dir)
799 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000800
801 self.afterInstall()
802
803 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000804 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000805 return "post-install %s: running \"%s\" failed" % \
806 (self.fullname(), self._dict['Post-install-command'])
807 return None
808
809
Jack Jansen95839b82003-02-09 23:10:20 +0000810class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000811 """Installer engine: computes dependencies and installs
812 packages in the right order."""
813
814 def __init__(self, db):
815 self._todo = []
816 self._db = db
817 self._curtodo = []
818 self._curmessages = []
819
820 def __contains__(self, package):
821 return package in self._todo
822
823 def _addPackages(self, packages):
824 for package in packages:
825 if not package in self._todo:
826 self._todo.insert(0, package)
827
828 def _prepareInstall(self, package, force=0, recursive=1):
829 """Internal routine, recursive engine for prepareInstall.
830
831 Test whether the package is installed and (if not installed
832 or if force==1) prepend it to the temporary todo list and
833 call ourselves recursively on all prerequisites."""
834
835 if not force:
836 status, message = package.installed()
837 if status == "yes":
838 return
839 if package in self._todo or package in self._curtodo:
840 return
841 self._curtodo.insert(0, package)
842 if not recursive:
843 return
844 prereqs = package.prerequisites()
845 for pkg, descr in prereqs:
846 if pkg:
847 self._prepareInstall(pkg, force, recursive)
848 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000849 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000850
851 def prepareInstall(self, package, force=0, recursive=1):
852 """Prepare installation of a package.
853
854 If the package is already installed and force is false nothing
855 is done. If recursive is true prerequisites are installed first.
856
857 Returns a list of packages (to be passed to install) and a list
858 of messages of any problems encountered.
859 """
860
861 self._curtodo = []
862 self._curmessages = []
863 self._prepareInstall(package, force, recursive)
864 rv = self._curtodo, self._curmessages
865 self._curtodo = []
866 self._curmessages = []
867 return rv
868
869 def install(self, packages, output):
870 """Install a list of packages."""
871
872 self._addPackages(packages)
873 status = []
874 for pkg in self._todo:
875 msg = pkg.installSinglePackage(output)
876 if msg:
877 status.append(msg)
878 return status
879
880
881
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000882def _run(mode, verbose, force, args, prefargs):
Jack Jansen0ae32202003-04-09 13:25:43 +0000883 """Engine for the main program"""
884
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000885 prefs = PimpPreferences(**prefargs)
886 rv = prefs.check()
887 if rv:
888 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000889 db = PimpDatabase(prefs)
890 db.appendURL(prefs.pimpDatabase)
891
892 if mode == 'dump':
893 db.dump(sys.stdout)
894 elif mode =='list':
895 if not args:
896 args = db.listnames()
897 print "%-20.20s\t%s" % ("Package", "Description")
898 print
899 for pkgname in args:
900 pkg = db.find(pkgname)
901 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000902 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000903 pkgname = pkg.fullname()
904 else:
905 description = 'Error: no such package'
906 print "%-20.20s\t%s" % (pkgname, description)
907 if verbose:
908 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000909 try:
910 print "\tDownload URL:\t", pkg.downloadURL()
911 except KeyError:
912 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +0000913 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000914 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +0000915 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +0000916 elif mode =='status':
917 if not args:
918 args = db.listnames()
919 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
920 print
921 for pkgname in args:
922 pkg = db.find(pkgname)
923 if pkg:
924 status, msg = pkg.installed()
925 pkgname = pkg.fullname()
926 else:
927 status = 'error'
928 msg = 'No such package'
929 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
930 if verbose and status == "no":
931 prereq = pkg.prerequisites()
932 for pkg, msg in prereq:
933 if not pkg:
934 pkg = ''
935 else:
936 pkg = pkg.fullname()
937 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
938 elif mode == 'install':
939 if not args:
940 print 'Please specify packages to install'
941 sys.exit(1)
942 inst = PimpInstaller(db)
943 for pkgname in args:
944 pkg = db.find(pkgname)
945 if not pkg:
946 print '%s: No such package' % pkgname
947 continue
948 list, messages = inst.prepareInstall(pkg, force)
949 if messages and not force:
950 print "%s: Not installed:" % pkgname
951 for m in messages:
952 print "\t", m
953 else:
954 if verbose:
955 output = sys.stdout
956 else:
957 output = None
958 messages = inst.install(list, output)
959 if messages:
960 print "%s: Not installed:" % pkgname
961 for m in messages:
962 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000963
964def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000965 """Minimal commandline tool to drive pimp."""
966
967 import getopt
968 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000969 print "Usage: pimp [options] -s [package ...] List installed status"
970 print " pimp [options] -l [package ...] Show package information"
971 print " pimp [options] -i package ... Install packages"
972 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +0000973 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +0000974 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000975 print " -v Verbose"
976 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +0000977 print " -D dir Set destination directory"
978 print " (default: %s)" % DEFAULT_INSTALLDIR
979 print " -u url URL for database"
Jack Jansen0ae32202003-04-09 13:25:43 +0000980 sys.exit(1)
981
982 try:
Jack Jansenb789a062003-05-28 18:56:30 +0000983 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
984 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +0000985 _help()
986 if not opts and not args:
987 _help()
988 mode = None
989 force = 0
990 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000991 prefargs = {}
Jack Jansen0ae32202003-04-09 13:25:43 +0000992 for o, a in opts:
993 if o == '-s':
994 if mode:
995 _help()
996 mode = 'status'
997 if o == '-l':
998 if mode:
999 _help()
1000 mode = 'list'
1001 if o == '-d':
1002 if mode:
1003 _help()
1004 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001005 if o == '-V':
1006 if mode:
1007 _help()
1008 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001009 if o == '-i':
1010 mode = 'install'
1011 if o == '-f':
1012 force = 1
1013 if o == '-v':
1014 verbose = 1
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001015 if o == '-D':
1016 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001017 if o == '-u':
1018 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001019 if not mode:
1020 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001021 if mode == 'version':
1022 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1023 else:
1024 _run(mode, verbose, force, args, prefargs)
1025
1026# Finally, try to update ourselves to a newer version.
1027# If the end-user updates pimp through pimp the new version
1028# will be called pimp_update and live in site-packages
1029# or somewhere similar
1030if __name__ != 'pimp_update':
1031 try:
1032 import pimp_update
1033 except ImportError:
1034 pass
1035 else:
1036 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1037 import warnings
1038 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1039 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1040 else:
1041 from pimp_update import *
1042
Jack Jansen95839b82003-02-09 23:10:20 +00001043if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001044 main()
1045
1046