blob: fc97ea38c0408a103f063615bcf050caa7c5d749 [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 Jansen6fde1ce2003-04-15 14:43:05 +000049def _cmd(output, dir, *cmditems):
50 """Internal routine to run a shell command in a given directory."""
51
52 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
53 if output:
54 output.write("+ %s\n" % cmd)
55 if NO_EXECUTE:
56 return 0
57 child = popen2.Popen4(cmd)
58 child.tochild.close()
59 while 1:
60 line = child.fromchild.readline()
61 if not line:
62 break
63 if output:
64 output.write(line)
65 return child.wait()
66
67class PimpUnpacker:
68 """Abstract base class - Unpacker for archives"""
69
70 _can_rename = False
71
72 def __init__(self, argument,
73 dir="",
74 renames=[]):
75 self.argument = argument
76 if renames and not self._can_rename:
77 raise RuntimeError, "This unpacker cannot rename files"
78 self._dir = dir
79 self._renames = renames
80
Jack Jansen5da131b2003-06-01 20:57:12 +000081 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +000082 return None
83
84class PimpCommandUnpacker(PimpUnpacker):
85 """Unpack archives by calling a Unix utility"""
86
87 _can_rename = False
88
Jack Jansen5da131b2003-06-01 20:57:12 +000089 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +000090 cmd = self.argument % archive
91 if _cmd(output, self._dir, cmd):
92 return "unpack command failed"
93
94class PimpTarUnpacker(PimpUnpacker):
95 """Unpack tarfiles using the builtin tarfile module"""
96
97 _can_rename = True
98
Jack Jansen5da131b2003-06-01 20:57:12 +000099 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000100 tf = tarfile.open(archive, "r")
101 members = tf.getmembers()
102 skip = []
103 if self._renames:
104 for member in members:
105 for oldprefix, newprefix in self._renames:
106 if oldprefix[:len(self._dir)] == self._dir:
107 oldprefix2 = oldprefix[len(self._dir):]
108 else:
109 oldprefix2 = None
110 if member.name[:len(oldprefix)] == oldprefix:
111 if newprefix is None:
112 skip.append(member)
113 #print 'SKIP', member.name
114 else:
115 member.name = newprefix + member.name[len(oldprefix):]
116 print ' ', member.name
117 break
118 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
119 if newprefix is None:
120 skip.append(member)
121 #print 'SKIP', member.name
122 else:
123 member.name = newprefix + member.name[len(oldprefix2):]
124 #print ' ', member.name
125 break
126 else:
127 skip.append(member)
128 #print '????', member.name
129 for member in members:
130 if member in skip:
131 continue
132 tf.extract(member, self._dir)
133 if skip:
134 names = [member.name for member in skip if member.name[-1] != '/']
Jack Jansen5da131b2003-06-01 20:57:12 +0000135 if package:
136 names = package.filterExpectedSkips(names)
Jack Jansen6432f782003-04-22 13:56:19 +0000137 if names:
Jack Jansen705553a2003-05-06 12:44:00 +0000138 return "Not all files were unpacked: %s" % " ".join(names)
Jack Jansen5da131b2003-06-01 20:57:12 +0000139
Jack Jansen95839b82003-02-09 23:10:20 +0000140ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000141 (".tar.Z", PimpTarUnpacker, None),
142 (".taz", PimpTarUnpacker, None),
143 (".tar.gz", PimpTarUnpacker, None),
144 (".tgz", PimpTarUnpacker, None),
145 (".tar.bz", PimpTarUnpacker, None),
146 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000147]
148
149class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000150 """Container for per-user preferences, such as the database to use
151 and where to install packages."""
152
153 def __init__(self,
154 flavorOrder=None,
155 downloadDir=None,
156 buildDir=None,
157 installDir=None,
158 pimpDatabase=None):
159 if not flavorOrder:
160 flavorOrder = DEFAULT_FLAVORORDER
161 if not downloadDir:
162 downloadDir = DEFAULT_DOWNLOADDIR
163 if not buildDir:
164 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000165 if not pimpDatabase:
Jack Jansenafd63b92004-02-28 22:34:02 +0000166 pimpDatabase = self.getDefaultDatabase()
Jack Jansen20fa6752003-04-16 12:15:34 +0000167 self.setInstallDir(installDir)
168 self.flavorOrder = flavorOrder
169 self.downloadDir = downloadDir
170 self.buildDir = buildDir
171 self.pimpDatabase = pimpDatabase
172
173 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000174 if installDir:
175 # Installing to non-standard location.
176 self.installLocations = [
177 ('--install-lib', installDir),
178 ('--install-headers', None),
179 ('--install-scripts', None),
180 ('--install-data', None)]
181 else:
182 installDir = DEFAULT_INSTALLDIR
183 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000184 self.installDir = installDir
Jack Jansen5da131b2003-06-01 20:57:12 +0000185
186 def isUserInstall(self):
187 return self.installDir != DEFAULT_INSTALLDIR
Jack Jansenafd63b92004-02-28 22:34:02 +0000188
189 def getDefaultDatabase(self, experimental=False):
190 if experimental:
191 status = "exp"
192 else:
193 status = "prod"
194
195 major, minor, micro, state, extra = sys.version_info
196 pyvers = '%d.%d' % (major, minor)
197 if state != 'final':
198 pyvers = pyvers + '%s%d' % (state, extra)
199
200 longplatform = distutils.util.get_platform()
201 osname, release, machine = longplatform.split('-')
202 # For some platforms we may want to differentiate between
203 # installation types
204 if osname == 'darwin':
205 if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'):
206 osname = 'darwin_apple'
207 elif sys.prefix.startswith('/Library/Frameworks/Python.framework'):
208 osname = 'darwin_macpython'
209 # Otherwise we don't know...
210 # Now we try various URLs by playing with the release string.
211 # We remove numbers off the end until we find a match.
212 rel = release
213 while True:
214 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine)
215 try:
216 urllib2.urlopen(url)
217 except urllib2.HTTPError, arg:
218 print 'getDefaultDatabase: cannot open', url
219 print 'error', arg
220 pass
221 else:
222 break
223 if not rel:
224 # We're out of version numbers to try. Use the
225 # full release number, this will give a reasonable
226 # error message later
227 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine)
228 break
229 idx = rel.rfind('.')
230 if idx < 0:
231 rel = ''
232 else:
233 rel = rel[:idx]
234 return url
Jack Jansen20fa6752003-04-16 12:15:34 +0000235
Jack Jansen0ae32202003-04-09 13:25:43 +0000236 def check(self):
237 """Check that the preferences make sense: directories exist and are
238 writable, the install directory is on sys.path, etc."""
239
240 rv = ""
241 RWX_OK = os.R_OK|os.W_OK|os.X_OK
242 if not os.path.exists(self.downloadDir):
243 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
244 elif not os.access(self.downloadDir, RWX_OK):
245 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
246 if not os.path.exists(self.buildDir):
247 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
248 elif not os.access(self.buildDir, RWX_OK):
249 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
250 if not os.path.exists(self.installDir):
251 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
252 elif not os.access(self.installDir, RWX_OK):
253 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
254 else:
255 installDir = os.path.realpath(self.installDir)
256 for p in sys.path:
257 try:
258 realpath = os.path.realpath(p)
259 except:
260 pass
261 if installDir == realpath:
262 break
263 else:
264 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000265 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000266
267 def compareFlavors(self, left, right):
268 """Compare two flavor strings. This is part of your preferences
269 because whether the user prefers installing from source or binary is."""
270 if left in self.flavorOrder:
271 if right in self.flavorOrder:
272 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
273 return -1
274 if right in self.flavorOrder:
275 return 1
276 return cmp(left, right)
277
Jack Jansen95839b82003-02-09 23:10:20 +0000278class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000279 """Class representing a pimp database. It can actually contain
280 information from multiple databases through inclusion, but the
281 toplevel database is considered the master, as its maintainer is
282 "responsible" for the contents."""
283
284 def __init__(self, prefs):
285 self._packages = []
286 self.preferences = prefs
287 self._urllist = []
288 self._version = ""
289 self._maintainer = ""
290 self._description = ""
291
292 def close(self):
293 """Clean up"""
294 self._packages = []
295 self.preferences = None
296
297 def appendURL(self, url, included=0):
298 """Append packages from the database with the given URL.
299 Only the first database should specify included=0, so the
300 global information (maintainer, description) get stored."""
301
302 if url in self._urllist:
303 return
304 self._urllist.append(url)
305 fp = urllib2.urlopen(url).fp
306 dict = plistlib.Plist.fromFile(fp)
307 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000308 if included:
309 version = dict.get('Version')
310 if version and version > self._version:
311 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
312 (url, version))
313 else:
314 self._version = dict.get('Version')
315 if not self._version:
316 sys.stderr.write("Warning: database has no Version information\n")
317 elif self._version > PIMP_VERSION:
318 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000319 % (self._version, PIMP_VERSION))
320 self._maintainer = dict.get('Maintainer', '')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000321 self._description = dict.get('Description', '').strip()
Jack Jansen0ae32202003-04-09 13:25:43 +0000322 self._appendPackages(dict['Packages'])
323 others = dict.get('Include', [])
324 for url in others:
325 self.appendURL(url, included=1)
326
327 def _appendPackages(self, packages):
328 """Given a list of dictionaries containing package
329 descriptions create the PimpPackage objects and append them
330 to our internal storage."""
331
332 for p in packages:
333 p = dict(p)
334 flavor = p.get('Flavor')
335 if flavor == 'source':
336 pkg = PimpPackage_source(self, p)
337 elif flavor == 'binary':
338 pkg = PimpPackage_binary(self, p)
339 else:
340 pkg = PimpPackage(self, dict(p))
341 self._packages.append(pkg)
342
343 def list(self):
344 """Return a list of all PimpPackage objects in the database."""
345
346 return self._packages
347
348 def listnames(self):
349 """Return a list of names of all packages in the database."""
350
351 rv = []
352 for pkg in self._packages:
353 rv.append(pkg.fullname())
354 rv.sort()
355 return rv
356
357 def dump(self, pathOrFile):
358 """Dump the contents of the database to an XML .plist file.
359
360 The file can be passed as either a file object or a pathname.
361 All data, including included databases, is dumped."""
362
363 packages = []
364 for pkg in self._packages:
365 packages.append(pkg.dump())
366 dict = {
367 'Version': self._version,
368 'Maintainer': self._maintainer,
369 'Description': self._description,
370 'Packages': packages
371 }
372 plist = plistlib.Plist(**dict)
373 plist.write(pathOrFile)
374
375 def find(self, ident):
376 """Find a package. The package can be specified by name
377 or as a dictionary with name, version and flavor entries.
378
379 Only name is obligatory. If there are multiple matches the
380 best one (higher version number, flavors ordered according to
381 users' preference) is returned."""
382
383 if type(ident) == str:
384 # Remove ( and ) for pseudo-packages
385 if ident[0] == '(' and ident[-1] == ')':
386 ident = ident[1:-1]
387 # Split into name-version-flavor
388 fields = ident.split('-')
389 if len(fields) < 1 or len(fields) > 3:
390 return None
391 name = fields[0]
392 if len(fields) > 1:
393 version = fields[1]
394 else:
395 version = None
396 if len(fields) > 2:
397 flavor = fields[2]
398 else:
399 flavor = None
400 else:
401 name = ident['Name']
402 version = ident.get('Version')
403 flavor = ident.get('Flavor')
404 found = None
405 for p in self._packages:
406 if name == p.name() and \
407 (not version or version == p.version()) and \
408 (not flavor or flavor == p.flavor()):
409 if not found or found < p:
410 found = p
411 return found
412
Jack Jansene7b33db2003-02-11 22:40:59 +0000413ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000414 "Name",
415 "Version",
416 "Flavor",
417 "Description",
418 "Home-page",
419 "Download-URL",
420 "Install-test",
421 "Install-command",
422 "Pre-install-command",
423 "Post-install-command",
424 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000425 "MD5Sum",
426 "User-install-skips",
427 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000428]
429
Jack Jansen95839b82003-02-09 23:10:20 +0000430class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000431 """Class representing a single package."""
432
433 def __init__(self, db, dict):
434 self._db = db
435 name = dict["Name"]
436 for k in dict.keys():
437 if not k in ALLOWED_KEYS:
438 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
439 self._dict = dict
440
441 def __getitem__(self, key):
442 return self._dict[key]
443
444 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000445 def version(self): return self._dict.get('Version')
446 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000447 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000448 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000449 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000450 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000451 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Jack Jansen0ae32202003-04-09 13:25:43 +0000452
453 def fullname(self):
454 """Return the full name "name-version-flavor" of a package.
455
456 If the package is a pseudo-package, something that cannot be
457 installed through pimp, return the name in (parentheses)."""
458
459 rv = self._dict['Name']
460 if self._dict.has_key('Version'):
461 rv = rv + '-%s' % self._dict['Version']
462 if self._dict.has_key('Flavor'):
463 rv = rv + '-%s' % self._dict['Flavor']
464 if not self._dict.get('Download-URL'):
465 # Pseudo-package, show in parentheses
466 rv = '(%s)' % rv
467 return rv
468
469 def dump(self):
470 """Return a dict object containing the information on the package."""
471 return self._dict
472
473 def __cmp__(self, other):
474 """Compare two packages, where the "better" package sorts lower."""
475
476 if not isinstance(other, PimpPackage):
477 return cmp(id(self), id(other))
478 if self.name() != other.name():
479 return cmp(self.name(), other.name())
480 if self.version() != other.version():
481 return -cmp(self.version(), other.version())
482 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
483
484 def installed(self):
485 """Test wheter the package is installed.
486
487 Returns two values: a status indicator which is one of
488 "yes", "no", "old" (an older version is installed) or "bad"
489 (something went wrong during the install test) and a human
490 readable string which may contain more details."""
491
492 namespace = {
493 "NotInstalled": _scriptExc_NotInstalled,
494 "OldInstalled": _scriptExc_OldInstalled,
495 "BadInstalled": _scriptExc_BadInstalled,
496 "os": os,
497 "sys": sys,
498 }
499 installTest = self._dict['Install-test'].strip() + '\n'
500 try:
501 exec installTest in namespace
502 except ImportError, arg:
503 return "no", str(arg)
504 except _scriptExc_NotInstalled, arg:
505 return "no", str(arg)
506 except _scriptExc_OldInstalled, arg:
507 return "old", str(arg)
508 except _scriptExc_BadInstalled, arg:
509 return "bad", str(arg)
510 except:
511 sys.stderr.write("-------------------------------------\n")
512 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
513 sys.stderr.write("---- source:\n")
514 sys.stderr.write(installTest)
515 sys.stderr.write("---- exception:\n")
516 import traceback
517 traceback.print_exc(file=sys.stderr)
518 if self._db._maintainer:
519 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
520 sys.stderr.write("-------------------------------------\n")
521 return "bad", "Package install test got exception"
522 return "yes", ""
523
524 def prerequisites(self):
525 """Return a list of prerequisites for this package.
526
527 The list contains 2-tuples, of which the first item is either
528 a PimpPackage object or None, and the second is a descriptive
529 string. The first item can be None if this package depends on
530 something that isn't pimp-installable, in which case the descriptive
531 string should tell the user what to do."""
532
533 rv = []
534 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000535 # For pseudo-packages that are already installed we don't
536 # return an error message
537 status, _ = self.installed()
538 if status == "yes":
539 return []
Jack Jansen0ae32202003-04-09 13:25:43 +0000540 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000541 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000542 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000543 if self.systemwideOnly() and self._db.preferences.isUserInstall():
544 return [(None,
545 "%s: This package can only be installed system-wide" %
546 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000547 if not self._dict.get('Prerequisites'):
548 return []
549 for item in self._dict['Prerequisites']:
550 if type(item) == str:
551 pkg = None
552 descr = str(item)
553 else:
554 name = item['Name']
555 if item.has_key('Version'):
556 name = name + '-' + item['Version']
557 if item.has_key('Flavor'):
558 name = name + '-' + item['Flavor']
559 pkg = self._db.find(name)
560 if not pkg:
561 descr = "Requires unknown %s"%name
562 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000563 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000564 rv.append((pkg, descr))
565 return rv
566
Jack Jansen0ae32202003-04-09 13:25:43 +0000567
568 def downloadPackageOnly(self, output=None):
569 """Download a single package, if needed.
570
571 An MD5 signature is used to determine whether download is needed,
572 and to test that we actually downloaded what we expected.
573 If output is given it is a file-like object that will receive a log
574 of what happens.
575
576 If anything unforeseen happened the method returns an error message
577 string.
578 """
579
580 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
581 path = urllib.url2pathname(path)
582 filename = os.path.split(path)[1]
583 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
584 if not self._archiveOK():
585 if scheme == 'manual':
586 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000587 if _cmd(output, self._db.preferences.downloadDir,
Jack Jansen0ae32202003-04-09 13:25:43 +0000588 "curl",
589 "--output", self.archiveFilename,
590 self._dict['Download-URL']):
591 return "download command failed"
592 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
593 return "archive not found after download"
594 if not self._archiveOK():
595 return "archive does not have correct MD5 checksum"
596
597 def _archiveOK(self):
598 """Test an archive. It should exist and the MD5 checksum should be correct."""
599
600 if not os.path.exists(self.archiveFilename):
601 return 0
602 if not self._dict.get('MD5Sum'):
603 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
604 return 1
605 data = open(self.archiveFilename, 'rb').read()
606 checksum = md5.new(data).hexdigest()
607 return checksum == self._dict['MD5Sum']
608
609 def unpackPackageOnly(self, output=None):
610 """Unpack a downloaded package archive."""
611
612 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000613 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000614 if filename[-len(ext):] == ext:
615 break
616 else:
617 return "unknown extension for archive file: %s" % filename
618 self.basename = filename[:-len(ext)]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000619 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir)
620 rv = unpacker.unpack(self.archiveFilename, output=output)
621 if rv:
622 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000623
624 def installPackageOnly(self, output=None):
625 """Default install method, to be overridden by subclasses"""
626 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
627 % (self.fullname(), self._dict.get(flavor, ""))
628
629 def installSinglePackage(self, output=None):
630 """Download, unpack and install a single package.
631
632 If output is given it should be a file-like object and it
633 will receive a log of what happened."""
634
Jack Jansen749f4812003-07-21 20:47:11 +0000635 if not self._dict.get('Download-URL'):
636 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000637 msg = self.downloadPackageOnly(output)
638 if msg:
639 return "%s: download: %s" % (self.fullname(), msg)
640
641 msg = self.unpackPackageOnly(output)
642 if msg:
643 return "%s: unpack: %s" % (self.fullname(), msg)
644
645 return self.installPackageOnly(output)
646
647 def beforeInstall(self):
648 """Bookkeeping before installation: remember what we have in site-packages"""
649 self._old_contents = os.listdir(self._db.preferences.installDir)
650
651 def afterInstall(self):
652 """Bookkeeping after installation: interpret any new .pth files that have
653 appeared"""
654
655 new_contents = os.listdir(self._db.preferences.installDir)
656 for fn in new_contents:
657 if fn in self._old_contents:
658 continue
659 if fn[-4:] != '.pth':
660 continue
661 fullname = os.path.join(self._db.preferences.installDir, fn)
662 f = open(fullname)
663 for line in f.readlines():
664 if not line:
665 continue
666 if line[0] == '#':
667 continue
668 if line[:6] == 'import':
669 exec line
670 continue
671 if line[-1] == '\n':
672 line = line[:-1]
673 if not os.path.isabs(line):
674 line = os.path.join(self._db.preferences.installDir, line)
675 line = os.path.realpath(line)
676 if not line in sys.path:
677 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000678
Jack Jansen5da131b2003-06-01 20:57:12 +0000679 def filterExpectedSkips(self, names):
680 """Return a list that contains only unpexpected skips"""
681 if not self._db.preferences.isUserInstall():
682 return names
683 expected_skips = self._dict.get('User-install-skips')
684 if not expected_skips:
685 return names
686 newnames = []
687 for name in names:
688 for skip in expected_skips:
689 if name[:len(skip)] == skip:
690 break
691 else:
692 newnames.append(name)
693 return newnames
694
Jack Jansen0dacac42003-02-14 14:11:59 +0000695class PimpPackage_binary(PimpPackage):
696
Jack Jansen0ae32202003-04-09 13:25:43 +0000697 def unpackPackageOnly(self, output=None):
698 """We don't unpack binary packages until installing"""
699 pass
700
701 def installPackageOnly(self, output=None):
702 """Install a single source package.
703
704 If output is given it should be a file-like object and it
705 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000706
Jack Jansen0ae32202003-04-09 13:25:43 +0000707 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000708 return "%s: Binary package cannot have Install-command" % self.fullname()
709
710 if self._dict.has_key('Pre-install-command'):
711 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
712 return "pre-install %s: running \"%s\" failed" % \
713 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000714
715 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000716
Jack Jansen0ae32202003-04-09 13:25:43 +0000717 # Install by unpacking
718 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000719 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000720 if filename[-len(ext):] == ext:
721 break
722 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000723 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
724 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000725
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000726 install_renames = []
727 for k, newloc in self._db.preferences.installLocations:
728 if not newloc:
729 continue
730 if k == "--install-lib":
731 oldloc = DEFAULT_INSTALLDIR
732 else:
733 return "%s: Don't know installLocation %s" % (self.fullname(), k)
734 install_renames.append((oldloc, newloc))
735
736 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000737 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000738 if rv:
739 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000740
741 self.afterInstall()
742
743 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000744 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
745 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000746 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000747
Jack Jansen0ae32202003-04-09 13:25:43 +0000748 return None
749
750
Jack Jansen0dacac42003-02-14 14:11:59 +0000751class PimpPackage_source(PimpPackage):
752
Jack Jansen0ae32202003-04-09 13:25:43 +0000753 def unpackPackageOnly(self, output=None):
754 """Unpack a source package and check that setup.py exists"""
755 PimpPackage.unpackPackageOnly(self, output)
756 # Test that a setup script has been create
757 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
758 setupname = os.path.join(self._buildDirname, "setup.py")
759 if not os.path.exists(setupname) and not NO_EXECUTE:
760 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000761
Jack Jansen0ae32202003-04-09 13:25:43 +0000762 def installPackageOnly(self, output=None):
763 """Install a single source package.
764
765 If output is given it should be a file-like object and it
766 will receive a log of what happened."""
767
768 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000769 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000770 return "pre-install %s: running \"%s\" failed" % \
771 (self.fullname(), self._dict['Pre-install-command'])
772
773 self.beforeInstall()
774 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000775 if installcmd and self._install_renames:
776 return "Package has install-command and can only be installed to standard location"
777 # This is the "bit-bucket" for installations: everything we don't
778 # want. After installation we check that it is actually empty
779 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000780 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000781 extra_args = ""
782 for k, v in self._db.preferences.installLocations:
783 if not v:
784 # We don't want these files installed. Send them
785 # to the bit-bucket.
786 if not unwanted_install_dir:
787 unwanted_install_dir = tempfile.mkdtemp()
788 v = unwanted_install_dir
789 extra_args = extra_args + " %s \"%s\"" % (k, v)
790 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
791 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000792 return "install %s: running \"%s\" failed" % \
793 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000794 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
795 unwanted_files = os.listdir(unwanted_install_dir)
796 if unwanted_files:
797 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
798 else:
799 rv = None
800 shutil.rmtree(unwanted_install_dir)
801 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000802
803 self.afterInstall()
804
805 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000806 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000807 return "post-install %s: running \"%s\" failed" % \
808 (self.fullname(), self._dict['Post-install-command'])
809 return None
810
811
Jack Jansen95839b82003-02-09 23:10:20 +0000812class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000813 """Installer engine: computes dependencies and installs
814 packages in the right order."""
815
816 def __init__(self, db):
817 self._todo = []
818 self._db = db
819 self._curtodo = []
820 self._curmessages = []
821
822 def __contains__(self, package):
823 return package in self._todo
824
825 def _addPackages(self, packages):
826 for package in packages:
827 if not package in self._todo:
828 self._todo.insert(0, package)
829
830 def _prepareInstall(self, package, force=0, recursive=1):
831 """Internal routine, recursive engine for prepareInstall.
832
833 Test whether the package is installed and (if not installed
834 or if force==1) prepend it to the temporary todo list and
835 call ourselves recursively on all prerequisites."""
836
837 if not force:
838 status, message = package.installed()
839 if status == "yes":
840 return
841 if package in self._todo or package in self._curtodo:
842 return
843 self._curtodo.insert(0, package)
844 if not recursive:
845 return
846 prereqs = package.prerequisites()
847 for pkg, descr in prereqs:
848 if pkg:
849 self._prepareInstall(pkg, force, recursive)
850 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000851 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000852
853 def prepareInstall(self, package, force=0, recursive=1):
854 """Prepare installation of a package.
855
856 If the package is already installed and force is false nothing
857 is done. If recursive is true prerequisites are installed first.
858
859 Returns a list of packages (to be passed to install) and a list
860 of messages of any problems encountered.
861 """
862
863 self._curtodo = []
864 self._curmessages = []
865 self._prepareInstall(package, force, recursive)
866 rv = self._curtodo, self._curmessages
867 self._curtodo = []
868 self._curmessages = []
869 return rv
870
871 def install(self, packages, output):
872 """Install a list of packages."""
873
874 self._addPackages(packages)
875 status = []
876 for pkg in self._todo:
877 msg = pkg.installSinglePackage(output)
878 if msg:
879 status.append(msg)
880 return status
881
882
883
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000884def _run(mode, verbose, force, args, prefargs):
Jack Jansen0ae32202003-04-09 13:25:43 +0000885 """Engine for the main program"""
886
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000887 prefs = PimpPreferences(**prefargs)
888 rv = prefs.check()
889 if rv:
890 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000891 db = PimpDatabase(prefs)
892 db.appendURL(prefs.pimpDatabase)
893
894 if mode == 'dump':
895 db.dump(sys.stdout)
896 elif mode =='list':
897 if not args:
898 args = db.listnames()
899 print "%-20.20s\t%s" % ("Package", "Description")
900 print
901 for pkgname in args:
902 pkg = db.find(pkgname)
903 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000904 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000905 pkgname = pkg.fullname()
906 else:
907 description = 'Error: no such package'
908 print "%-20.20s\t%s" % (pkgname, description)
909 if verbose:
910 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000911 try:
912 print "\tDownload URL:\t", pkg.downloadURL()
913 except KeyError:
914 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +0000915 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000916 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +0000917 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +0000918 elif mode =='status':
919 if not args:
920 args = db.listnames()
921 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
922 print
923 for pkgname in args:
924 pkg = db.find(pkgname)
925 if pkg:
926 status, msg = pkg.installed()
927 pkgname = pkg.fullname()
928 else:
929 status = 'error'
930 msg = 'No such package'
931 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
932 if verbose and status == "no":
933 prereq = pkg.prerequisites()
934 for pkg, msg in prereq:
935 if not pkg:
936 pkg = ''
937 else:
938 pkg = pkg.fullname()
939 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
940 elif mode == 'install':
941 if not args:
942 print 'Please specify packages to install'
943 sys.exit(1)
944 inst = PimpInstaller(db)
945 for pkgname in args:
946 pkg = db.find(pkgname)
947 if not pkg:
948 print '%s: No such package' % pkgname
949 continue
950 list, messages = inst.prepareInstall(pkg, force)
951 if messages and not force:
952 print "%s: Not installed:" % pkgname
953 for m in messages:
954 print "\t", m
955 else:
956 if verbose:
957 output = sys.stdout
958 else:
959 output = None
960 messages = inst.install(list, output)
961 if messages:
962 print "%s: Not installed:" % pkgname
963 for m in messages:
964 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000965
966def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000967 """Minimal commandline tool to drive pimp."""
968
969 import getopt
970 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000971 print "Usage: pimp [options] -s [package ...] List installed status"
972 print " pimp [options] -l [package ...] Show package information"
973 print " pimp [options] -i package ... Install packages"
974 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +0000975 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +0000976 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000977 print " -v Verbose"
978 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +0000979 print " -D dir Set destination directory"
980 print " (default: %s)" % DEFAULT_INSTALLDIR
981 print " -u url URL for database"
Jack Jansen0ae32202003-04-09 13:25:43 +0000982 sys.exit(1)
983
984 try:
Jack Jansenb789a062003-05-28 18:56:30 +0000985 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
986 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +0000987 _help()
988 if not opts and not args:
989 _help()
990 mode = None
991 force = 0
992 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000993 prefargs = {}
Jack Jansen0ae32202003-04-09 13:25:43 +0000994 for o, a in opts:
995 if o == '-s':
996 if mode:
997 _help()
998 mode = 'status'
999 if o == '-l':
1000 if mode:
1001 _help()
1002 mode = 'list'
1003 if o == '-d':
1004 if mode:
1005 _help()
1006 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001007 if o == '-V':
1008 if mode:
1009 _help()
1010 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001011 if o == '-i':
1012 mode = 'install'
1013 if o == '-f':
1014 force = 1
1015 if o == '-v':
1016 verbose = 1
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001017 if o == '-D':
1018 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001019 if o == '-u':
1020 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001021 if not mode:
1022 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001023 if mode == 'version':
1024 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1025 else:
1026 _run(mode, verbose, force, args, prefargs)
1027
1028# Finally, try to update ourselves to a newer version.
1029# If the end-user updates pimp through pimp the new version
1030# will be called pimp_update and live in site-packages
1031# or somewhere similar
1032if __name__ != 'pimp_update':
1033 try:
1034 import pimp_update
1035 except ImportError:
1036 pass
1037 else:
1038 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1039 import warnings
1040 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1041 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1042 else:
1043 from pimp_update import *
1044
Jack Jansen95839b82003-02-09 23:10:20 +00001045if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001046 main()
1047
1048