blob: b05767bee3ee1ec46916ea9466879d49e8550c13 [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 Jansenc5991b02003-06-29 00:09:18 +000038PIMP_VERSION="0.3"
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 Jansenc5991b02003-06-29 00:09:18 +000047DEFAULT_PIMPDATABASE="http://www.python.org/packman/version-0.3/%s.plist" % distutils.util.get_platform()
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:
166 pimpDatabase = DEFAULT_PIMPDATABASE
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 Jansen20fa6752003-04-16 12:15:34 +0000188
Jack Jansen0ae32202003-04-09 13:25:43 +0000189 def check(self):
190 """Check that the preferences make sense: directories exist and are
191 writable, the install directory is on sys.path, etc."""
192
193 rv = ""
194 RWX_OK = os.R_OK|os.W_OK|os.X_OK
195 if not os.path.exists(self.downloadDir):
196 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
197 elif not os.access(self.downloadDir, RWX_OK):
198 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
199 if not os.path.exists(self.buildDir):
200 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
201 elif not os.access(self.buildDir, RWX_OK):
202 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
203 if not os.path.exists(self.installDir):
204 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
205 elif not os.access(self.installDir, RWX_OK):
206 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
207 else:
208 installDir = os.path.realpath(self.installDir)
209 for p in sys.path:
210 try:
211 realpath = os.path.realpath(p)
212 except:
213 pass
214 if installDir == realpath:
215 break
216 else:
217 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000218 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000219
220 def compareFlavors(self, left, right):
221 """Compare two flavor strings. This is part of your preferences
222 because whether the user prefers installing from source or binary is."""
223 if left in self.flavorOrder:
224 if right in self.flavorOrder:
225 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
226 return -1
227 if right in self.flavorOrder:
228 return 1
229 return cmp(left, right)
230
Jack Jansen95839b82003-02-09 23:10:20 +0000231class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000232 """Class representing a pimp database. It can actually contain
233 information from multiple databases through inclusion, but the
234 toplevel database is considered the master, as its maintainer is
235 "responsible" for the contents."""
236
237 def __init__(self, prefs):
238 self._packages = []
239 self.preferences = prefs
240 self._urllist = []
241 self._version = ""
242 self._maintainer = ""
243 self._description = ""
244
245 def close(self):
246 """Clean up"""
247 self._packages = []
248 self.preferences = None
249
250 def appendURL(self, url, included=0):
251 """Append packages from the database with the given URL.
252 Only the first database should specify included=0, so the
253 global information (maintainer, description) get stored."""
254
255 if url in self._urllist:
256 return
257 self._urllist.append(url)
258 fp = urllib2.urlopen(url).fp
259 dict = plistlib.Plist.fromFile(fp)
260 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000261 if included:
262 version = dict.get('Version')
263 if version and version > self._version:
264 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
265 (url, version))
266 else:
267 self._version = dict.get('Version')
268 if not self._version:
269 sys.stderr.write("Warning: database has no Version information\n")
270 elif self._version > PIMP_VERSION:
271 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000272 % (self._version, PIMP_VERSION))
273 self._maintainer = dict.get('Maintainer', '')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000274 self._description = dict.get('Description', '').strip()
Jack Jansen0ae32202003-04-09 13:25:43 +0000275 self._appendPackages(dict['Packages'])
276 others = dict.get('Include', [])
277 for url in others:
278 self.appendURL(url, included=1)
279
280 def _appendPackages(self, packages):
281 """Given a list of dictionaries containing package
282 descriptions create the PimpPackage objects and append them
283 to our internal storage."""
284
285 for p in packages:
286 p = dict(p)
287 flavor = p.get('Flavor')
288 if flavor == 'source':
289 pkg = PimpPackage_source(self, p)
290 elif flavor == 'binary':
291 pkg = PimpPackage_binary(self, p)
292 else:
293 pkg = PimpPackage(self, dict(p))
294 self._packages.append(pkg)
295
296 def list(self):
297 """Return a list of all PimpPackage objects in the database."""
298
299 return self._packages
300
301 def listnames(self):
302 """Return a list of names of all packages in the database."""
303
304 rv = []
305 for pkg in self._packages:
306 rv.append(pkg.fullname())
307 rv.sort()
308 return rv
309
310 def dump(self, pathOrFile):
311 """Dump the contents of the database to an XML .plist file.
312
313 The file can be passed as either a file object or a pathname.
314 All data, including included databases, is dumped."""
315
316 packages = []
317 for pkg in self._packages:
318 packages.append(pkg.dump())
319 dict = {
320 'Version': self._version,
321 'Maintainer': self._maintainer,
322 'Description': self._description,
323 'Packages': packages
324 }
325 plist = plistlib.Plist(**dict)
326 plist.write(pathOrFile)
327
328 def find(self, ident):
329 """Find a package. The package can be specified by name
330 or as a dictionary with name, version and flavor entries.
331
332 Only name is obligatory. If there are multiple matches the
333 best one (higher version number, flavors ordered according to
334 users' preference) is returned."""
335
336 if type(ident) == str:
337 # Remove ( and ) for pseudo-packages
338 if ident[0] == '(' and ident[-1] == ')':
339 ident = ident[1:-1]
340 # Split into name-version-flavor
341 fields = ident.split('-')
342 if len(fields) < 1 or len(fields) > 3:
343 return None
344 name = fields[0]
345 if len(fields) > 1:
346 version = fields[1]
347 else:
348 version = None
349 if len(fields) > 2:
350 flavor = fields[2]
351 else:
352 flavor = None
353 else:
354 name = ident['Name']
355 version = ident.get('Version')
356 flavor = ident.get('Flavor')
357 found = None
358 for p in self._packages:
359 if name == p.name() and \
360 (not version or version == p.version()) and \
361 (not flavor or flavor == p.flavor()):
362 if not found or found < p:
363 found = p
364 return found
365
Jack Jansene7b33db2003-02-11 22:40:59 +0000366ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000367 "Name",
368 "Version",
369 "Flavor",
370 "Description",
371 "Home-page",
372 "Download-URL",
373 "Install-test",
374 "Install-command",
375 "Pre-install-command",
376 "Post-install-command",
377 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000378 "MD5Sum",
379 "User-install-skips",
380 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000381]
382
Jack Jansen95839b82003-02-09 23:10:20 +0000383class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000384 """Class representing a single package."""
385
386 def __init__(self, db, dict):
387 self._db = db
388 name = dict["Name"]
389 for k in dict.keys():
390 if not k in ALLOWED_KEYS:
391 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
392 self._dict = dict
393
394 def __getitem__(self, key):
395 return self._dict[key]
396
397 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000398 def version(self): return self._dict.get('Version')
399 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000400 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000401 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000402 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000403 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000404 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Jack Jansen0ae32202003-04-09 13:25:43 +0000405
406 def fullname(self):
407 """Return the full name "name-version-flavor" of a package.
408
409 If the package is a pseudo-package, something that cannot be
410 installed through pimp, return the name in (parentheses)."""
411
412 rv = self._dict['Name']
413 if self._dict.has_key('Version'):
414 rv = rv + '-%s' % self._dict['Version']
415 if self._dict.has_key('Flavor'):
416 rv = rv + '-%s' % self._dict['Flavor']
417 if not self._dict.get('Download-URL'):
418 # Pseudo-package, show in parentheses
419 rv = '(%s)' % rv
420 return rv
421
422 def dump(self):
423 """Return a dict object containing the information on the package."""
424 return self._dict
425
426 def __cmp__(self, other):
427 """Compare two packages, where the "better" package sorts lower."""
428
429 if not isinstance(other, PimpPackage):
430 return cmp(id(self), id(other))
431 if self.name() != other.name():
432 return cmp(self.name(), other.name())
433 if self.version() != other.version():
434 return -cmp(self.version(), other.version())
435 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
436
437 def installed(self):
438 """Test wheter the package is installed.
439
440 Returns two values: a status indicator which is one of
441 "yes", "no", "old" (an older version is installed) or "bad"
442 (something went wrong during the install test) and a human
443 readable string which may contain more details."""
444
445 namespace = {
446 "NotInstalled": _scriptExc_NotInstalled,
447 "OldInstalled": _scriptExc_OldInstalled,
448 "BadInstalled": _scriptExc_BadInstalled,
449 "os": os,
450 "sys": sys,
451 }
452 installTest = self._dict['Install-test'].strip() + '\n'
453 try:
454 exec installTest in namespace
455 except ImportError, arg:
456 return "no", str(arg)
457 except _scriptExc_NotInstalled, arg:
458 return "no", str(arg)
459 except _scriptExc_OldInstalled, arg:
460 return "old", str(arg)
461 except _scriptExc_BadInstalled, arg:
462 return "bad", str(arg)
463 except:
464 sys.stderr.write("-------------------------------------\n")
465 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
466 sys.stderr.write("---- source:\n")
467 sys.stderr.write(installTest)
468 sys.stderr.write("---- exception:\n")
469 import traceback
470 traceback.print_exc(file=sys.stderr)
471 if self._db._maintainer:
472 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
473 sys.stderr.write("-------------------------------------\n")
474 return "bad", "Package install test got exception"
475 return "yes", ""
476
477 def prerequisites(self):
478 """Return a list of prerequisites for this package.
479
480 The list contains 2-tuples, of which the first item is either
481 a PimpPackage object or None, and the second is a descriptive
482 string. The first item can be None if this package depends on
483 something that isn't pimp-installable, in which case the descriptive
484 string should tell the user what to do."""
485
486 rv = []
487 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000488 # For pseudo-packages that are already installed we don't
489 # return an error message
490 status, _ = self.installed()
491 if status == "yes":
492 return []
Jack Jansen0ae32202003-04-09 13:25:43 +0000493 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000494 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000495 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000496 if self.systemwideOnly() and self._db.preferences.isUserInstall():
497 return [(None,
498 "%s: This package can only be installed system-wide" %
499 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000500 if not self._dict.get('Prerequisites'):
501 return []
502 for item in self._dict['Prerequisites']:
503 if type(item) == str:
504 pkg = None
505 descr = str(item)
506 else:
507 name = item['Name']
508 if item.has_key('Version'):
509 name = name + '-' + item['Version']
510 if item.has_key('Flavor'):
511 name = name + '-' + item['Flavor']
512 pkg = self._db.find(name)
513 if not pkg:
514 descr = "Requires unknown %s"%name
515 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000516 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000517 rv.append((pkg, descr))
518 return rv
519
Jack Jansen0ae32202003-04-09 13:25:43 +0000520
521 def downloadPackageOnly(self, output=None):
522 """Download a single package, if needed.
523
524 An MD5 signature is used to determine whether download is needed,
525 and to test that we actually downloaded what we expected.
526 If output is given it is a file-like object that will receive a log
527 of what happens.
528
529 If anything unforeseen happened the method returns an error message
530 string.
531 """
532
533 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
534 path = urllib.url2pathname(path)
535 filename = os.path.split(path)[1]
536 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
537 if not self._archiveOK():
538 if scheme == 'manual':
539 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000540 if _cmd(output, self._db.preferences.downloadDir,
Jack Jansen0ae32202003-04-09 13:25:43 +0000541 "curl",
542 "--output", self.archiveFilename,
543 self._dict['Download-URL']):
544 return "download command failed"
545 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
546 return "archive not found after download"
547 if not self._archiveOK():
548 return "archive does not have correct MD5 checksum"
549
550 def _archiveOK(self):
551 """Test an archive. It should exist and the MD5 checksum should be correct."""
552
553 if not os.path.exists(self.archiveFilename):
554 return 0
555 if not self._dict.get('MD5Sum'):
556 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
557 return 1
558 data = open(self.archiveFilename, 'rb').read()
559 checksum = md5.new(data).hexdigest()
560 return checksum == self._dict['MD5Sum']
561
562 def unpackPackageOnly(self, output=None):
563 """Unpack a downloaded package archive."""
564
565 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000566 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000567 if filename[-len(ext):] == ext:
568 break
569 else:
570 return "unknown extension for archive file: %s" % filename
571 self.basename = filename[:-len(ext)]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000572 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir)
573 rv = unpacker.unpack(self.archiveFilename, output=output)
574 if rv:
575 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000576
577 def installPackageOnly(self, output=None):
578 """Default install method, to be overridden by subclasses"""
579 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
580 % (self.fullname(), self._dict.get(flavor, ""))
581
582 def installSinglePackage(self, output=None):
583 """Download, unpack and install a single package.
584
585 If output is given it should be a file-like object and it
586 will receive a log of what happened."""
587
588 if not self._dict['Download-URL']:
589 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
590 msg = self.downloadPackageOnly(output)
591 if msg:
592 return "%s: download: %s" % (self.fullname(), msg)
593
594 msg = self.unpackPackageOnly(output)
595 if msg:
596 return "%s: unpack: %s" % (self.fullname(), msg)
597
598 return self.installPackageOnly(output)
599
600 def beforeInstall(self):
601 """Bookkeeping before installation: remember what we have in site-packages"""
602 self._old_contents = os.listdir(self._db.preferences.installDir)
603
604 def afterInstall(self):
605 """Bookkeeping after installation: interpret any new .pth files that have
606 appeared"""
607
608 new_contents = os.listdir(self._db.preferences.installDir)
609 for fn in new_contents:
610 if fn in self._old_contents:
611 continue
612 if fn[-4:] != '.pth':
613 continue
614 fullname = os.path.join(self._db.preferences.installDir, fn)
615 f = open(fullname)
616 for line in f.readlines():
617 if not line:
618 continue
619 if line[0] == '#':
620 continue
621 if line[:6] == 'import':
622 exec line
623 continue
624 if line[-1] == '\n':
625 line = line[:-1]
626 if not os.path.isabs(line):
627 line = os.path.join(self._db.preferences.installDir, line)
628 line = os.path.realpath(line)
629 if not line in sys.path:
630 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000631
Jack Jansen5da131b2003-06-01 20:57:12 +0000632 def filterExpectedSkips(self, names):
633 """Return a list that contains only unpexpected skips"""
634 if not self._db.preferences.isUserInstall():
635 return names
636 expected_skips = self._dict.get('User-install-skips')
637 if not expected_skips:
638 return names
639 newnames = []
640 for name in names:
641 for skip in expected_skips:
642 if name[:len(skip)] == skip:
643 break
644 else:
645 newnames.append(name)
646 return newnames
647
Jack Jansen0dacac42003-02-14 14:11:59 +0000648class PimpPackage_binary(PimpPackage):
649
Jack Jansen0ae32202003-04-09 13:25:43 +0000650 def unpackPackageOnly(self, output=None):
651 """We don't unpack binary packages until installing"""
652 pass
653
654 def installPackageOnly(self, output=None):
655 """Install a single source package.
656
657 If output is given it should be a file-like object and it
658 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000659
Jack Jansen0ae32202003-04-09 13:25:43 +0000660 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000661 return "%s: Binary package cannot have Install-command" % self.fullname()
662
663 if self._dict.has_key('Pre-install-command'):
664 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
665 return "pre-install %s: running \"%s\" failed" % \
666 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000667
668 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000669
Jack Jansen0ae32202003-04-09 13:25:43 +0000670 # Install by unpacking
671 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000672 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000673 if filename[-len(ext):] == ext:
674 break
675 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000676 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
677 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000678
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000679 install_renames = []
680 for k, newloc in self._db.preferences.installLocations:
681 if not newloc:
682 continue
683 if k == "--install-lib":
684 oldloc = DEFAULT_INSTALLDIR
685 else:
686 return "%s: Don't know installLocation %s" % (self.fullname(), k)
687 install_renames.append((oldloc, newloc))
688
689 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000690 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000691 if rv:
692 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000693
694 self.afterInstall()
695
696 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000697 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
698 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000699 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000700
Jack Jansen0ae32202003-04-09 13:25:43 +0000701 return None
702
703
Jack Jansen0dacac42003-02-14 14:11:59 +0000704class PimpPackage_source(PimpPackage):
705
Jack Jansen0ae32202003-04-09 13:25:43 +0000706 def unpackPackageOnly(self, output=None):
707 """Unpack a source package and check that setup.py exists"""
708 PimpPackage.unpackPackageOnly(self, output)
709 # Test that a setup script has been create
710 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
711 setupname = os.path.join(self._buildDirname, "setup.py")
712 if not os.path.exists(setupname) and not NO_EXECUTE:
713 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000714
Jack Jansen0ae32202003-04-09 13:25:43 +0000715 def installPackageOnly(self, output=None):
716 """Install a single source package.
717
718 If output is given it should be a file-like object and it
719 will receive a log of what happened."""
720
721 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000722 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000723 return "pre-install %s: running \"%s\" failed" % \
724 (self.fullname(), self._dict['Pre-install-command'])
725
726 self.beforeInstall()
727 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000728 if installcmd and self._install_renames:
729 return "Package has install-command and can only be installed to standard location"
730 # This is the "bit-bucket" for installations: everything we don't
731 # want. After installation we check that it is actually empty
732 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000733 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000734 extra_args = ""
735 for k, v in self._db.preferences.installLocations:
736 if not v:
737 # We don't want these files installed. Send them
738 # to the bit-bucket.
739 if not unwanted_install_dir:
740 unwanted_install_dir = tempfile.mkdtemp()
741 v = unwanted_install_dir
742 extra_args = extra_args + " %s \"%s\"" % (k, v)
743 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
744 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000745 return "install %s: running \"%s\" failed" % \
746 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000747 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
748 unwanted_files = os.listdir(unwanted_install_dir)
749 if unwanted_files:
750 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
751 else:
752 rv = None
753 shutil.rmtree(unwanted_install_dir)
754 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000755
756 self.afterInstall()
757
758 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000759 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000760 return "post-install %s: running \"%s\" failed" % \
761 (self.fullname(), self._dict['Post-install-command'])
762 return None
763
764
Jack Jansen95839b82003-02-09 23:10:20 +0000765class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000766 """Installer engine: computes dependencies and installs
767 packages in the right order."""
768
769 def __init__(self, db):
770 self._todo = []
771 self._db = db
772 self._curtodo = []
773 self._curmessages = []
774
775 def __contains__(self, package):
776 return package in self._todo
777
778 def _addPackages(self, packages):
779 for package in packages:
780 if not package in self._todo:
781 self._todo.insert(0, package)
782
783 def _prepareInstall(self, package, force=0, recursive=1):
784 """Internal routine, recursive engine for prepareInstall.
785
786 Test whether the package is installed and (if not installed
787 or if force==1) prepend it to the temporary todo list and
788 call ourselves recursively on all prerequisites."""
789
790 if not force:
791 status, message = package.installed()
792 if status == "yes":
793 return
794 if package in self._todo or package in self._curtodo:
795 return
796 self._curtodo.insert(0, package)
797 if not recursive:
798 return
799 prereqs = package.prerequisites()
800 for pkg, descr in prereqs:
801 if pkg:
802 self._prepareInstall(pkg, force, recursive)
803 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000804 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000805
806 def prepareInstall(self, package, force=0, recursive=1):
807 """Prepare installation of a package.
808
809 If the package is already installed and force is false nothing
810 is done. If recursive is true prerequisites are installed first.
811
812 Returns a list of packages (to be passed to install) and a list
813 of messages of any problems encountered.
814 """
815
816 self._curtodo = []
817 self._curmessages = []
818 self._prepareInstall(package, force, recursive)
819 rv = self._curtodo, self._curmessages
820 self._curtodo = []
821 self._curmessages = []
822 return rv
823
824 def install(self, packages, output):
825 """Install a list of packages."""
826
827 self._addPackages(packages)
828 status = []
829 for pkg in self._todo:
830 msg = pkg.installSinglePackage(output)
831 if msg:
832 status.append(msg)
833 return status
834
835
836
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000837def _run(mode, verbose, force, args, prefargs):
Jack Jansen0ae32202003-04-09 13:25:43 +0000838 """Engine for the main program"""
839
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000840 prefs = PimpPreferences(**prefargs)
841 rv = prefs.check()
842 if rv:
843 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000844 db = PimpDatabase(prefs)
845 db.appendURL(prefs.pimpDatabase)
846
847 if mode == 'dump':
848 db.dump(sys.stdout)
849 elif mode =='list':
850 if not args:
851 args = db.listnames()
852 print "%-20.20s\t%s" % ("Package", "Description")
853 print
854 for pkgname in args:
855 pkg = db.find(pkgname)
856 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000857 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000858 pkgname = pkg.fullname()
859 else:
860 description = 'Error: no such package'
861 print "%-20.20s\t%s" % (pkgname, description)
862 if verbose:
863 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000864 try:
865 print "\tDownload URL:\t", pkg.downloadURL()
866 except KeyError:
867 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +0000868 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000869 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +0000870 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +0000871 elif mode =='status':
872 if not args:
873 args = db.listnames()
874 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
875 print
876 for pkgname in args:
877 pkg = db.find(pkgname)
878 if pkg:
879 status, msg = pkg.installed()
880 pkgname = pkg.fullname()
881 else:
882 status = 'error'
883 msg = 'No such package'
884 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
885 if verbose and status == "no":
886 prereq = pkg.prerequisites()
887 for pkg, msg in prereq:
888 if not pkg:
889 pkg = ''
890 else:
891 pkg = pkg.fullname()
892 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
893 elif mode == 'install':
894 if not args:
895 print 'Please specify packages to install'
896 sys.exit(1)
897 inst = PimpInstaller(db)
898 for pkgname in args:
899 pkg = db.find(pkgname)
900 if not pkg:
901 print '%s: No such package' % pkgname
902 continue
903 list, messages = inst.prepareInstall(pkg, force)
904 if messages and not force:
905 print "%s: Not installed:" % pkgname
906 for m in messages:
907 print "\t", m
908 else:
909 if verbose:
910 output = sys.stdout
911 else:
912 output = None
913 messages = inst.install(list, output)
914 if messages:
915 print "%s: Not installed:" % pkgname
916 for m in messages:
917 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000918
919def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000920 """Minimal commandline tool to drive pimp."""
921
922 import getopt
923 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000924 print "Usage: pimp [options] -s [package ...] List installed status"
925 print " pimp [options] -l [package ...] Show package information"
926 print " pimp [options] -i package ... Install packages"
927 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +0000928 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +0000929 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000930 print " -v Verbose"
931 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +0000932 print " -D dir Set destination directory"
933 print " (default: %s)" % DEFAULT_INSTALLDIR
934 print " -u url URL for database"
935 print " (default: %s)" % DEFAULT_PIMPDATABASE
Jack Jansen0ae32202003-04-09 13:25:43 +0000936 sys.exit(1)
937
938 try:
Jack Jansenb789a062003-05-28 18:56:30 +0000939 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
940 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +0000941 _help()
942 if not opts and not args:
943 _help()
944 mode = None
945 force = 0
946 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000947 prefargs = {}
Jack Jansen0ae32202003-04-09 13:25:43 +0000948 for o, a in opts:
949 if o == '-s':
950 if mode:
951 _help()
952 mode = 'status'
953 if o == '-l':
954 if mode:
955 _help()
956 mode = 'list'
957 if o == '-d':
958 if mode:
959 _help()
960 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +0000961 if o == '-V':
962 if mode:
963 _help()
964 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +0000965 if o == '-i':
966 mode = 'install'
967 if o == '-f':
968 force = 1
969 if o == '-v':
970 verbose = 1
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000971 if o == '-D':
972 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +0000973 if o == '-u':
974 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +0000975 if not mode:
976 _help()
Jack Jansenb789a062003-05-28 18:56:30 +0000977 if mode == 'version':
978 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
979 else:
980 _run(mode, verbose, force, args, prefargs)
981
982# Finally, try to update ourselves to a newer version.
983# If the end-user updates pimp through pimp the new version
984# will be called pimp_update and live in site-packages
985# or somewhere similar
986if __name__ != 'pimp_update':
987 try:
988 import pimp_update
989 except ImportError:
990 pass
991 else:
992 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
993 import warnings
994 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
995 (pimp_update.PIMP_VERSION, PIMP_VERSION))
996 else:
997 from pimp_update import *
998
Jack Jansen95839b82003-02-09 23:10:20 +0000999if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001000 main()
1001
1002