blob: 647fe8e233ec14ab29e1484d72f708c464edd68b [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.
4Motto: "He may be shabby, but he gets you what you need" :-)
5
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 Jansen6a600ab2003-02-10 15:55:51 +000029__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main"]
30
Jack Jansen95839b82003-02-09 23:10:20 +000031_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
32_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
33_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
34
35NO_EXECUTE=0
36
Jack Jansene7b33db2003-02-11 22:40:59 +000037PIMP_VERSION="0.1"
38
Jack Jansen0dacac42003-02-14 14:11:59 +000039# Flavors:
40# source: setup-based package
41# binary: tar (or other) archive created with setup.py bdist.
Jack Jansen95839b82003-02-09 23:10:20 +000042DEFAULT_FLAVORORDER=['source', 'binary']
43DEFAULT_DOWNLOADDIR='/tmp'
44DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000045DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansen4f450112003-05-02 20:27:40 +000046DEFAULT_PIMPDATABASE="http://homepages.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
Jack Jansen95839b82003-02-09 23:10:20 +000047
Jack Jansen6fde1ce2003-04-15 14:43:05 +000048def _cmd(output, dir, *cmditems):
49 """Internal routine to run a shell command in a given directory."""
50
51 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
52 if output:
53 output.write("+ %s\n" % cmd)
54 if NO_EXECUTE:
55 return 0
56 child = popen2.Popen4(cmd)
57 child.tochild.close()
58 while 1:
59 line = child.fromchild.readline()
60 if not line:
61 break
62 if output:
63 output.write(line)
64 return child.wait()
65
66class PimpUnpacker:
67 """Abstract base class - Unpacker for archives"""
68
69 _can_rename = False
70
71 def __init__(self, argument,
72 dir="",
73 renames=[]):
74 self.argument = argument
75 if renames and not self._can_rename:
76 raise RuntimeError, "This unpacker cannot rename files"
77 self._dir = dir
78 self._renames = renames
79
80 def unpack(self, archive, output=None):
81 return None
82
83class PimpCommandUnpacker(PimpUnpacker):
84 """Unpack archives by calling a Unix utility"""
85
86 _can_rename = False
87
88 def unpack(self, archive, output=None):
89 cmd = self.argument % archive
90 if _cmd(output, self._dir, cmd):
91 return "unpack command failed"
92
93class PimpTarUnpacker(PimpUnpacker):
94 """Unpack tarfiles using the builtin tarfile module"""
95
96 _can_rename = True
97
98 def unpack(self, archive, output=None):
99 tf = tarfile.open(archive, "r")
100 members = tf.getmembers()
101 skip = []
102 if self._renames:
103 for member in members:
104 for oldprefix, newprefix in self._renames:
105 if oldprefix[:len(self._dir)] == self._dir:
106 oldprefix2 = oldprefix[len(self._dir):]
107 else:
108 oldprefix2 = None
109 if member.name[:len(oldprefix)] == oldprefix:
110 if newprefix is None:
111 skip.append(member)
112 #print 'SKIP', member.name
113 else:
114 member.name = newprefix + member.name[len(oldprefix):]
115 print ' ', member.name
116 break
117 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
118 if newprefix is None:
119 skip.append(member)
120 #print 'SKIP', member.name
121 else:
122 member.name = newprefix + member.name[len(oldprefix2):]
123 #print ' ', member.name
124 break
125 else:
126 skip.append(member)
127 #print '????', member.name
128 for member in members:
129 if member in skip:
130 continue
131 tf.extract(member, self._dir)
132 if skip:
133 names = [member.name for member in skip if member.name[-1] != '/']
Jack Jansen6432f782003-04-22 13:56:19 +0000134 if names:
Jack Jansen705553a2003-05-06 12:44:00 +0000135 return "Not all files were unpacked: %s" % " ".join(names)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000136
Jack Jansen95839b82003-02-09 23:10:20 +0000137ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000138 (".tar.Z", PimpTarUnpacker, None),
139 (".taz", PimpTarUnpacker, None),
140 (".tar.gz", PimpTarUnpacker, None),
141 (".tgz", PimpTarUnpacker, None),
142 (".tar.bz", PimpTarUnpacker, None),
143 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000144]
145
146class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000147 """Container for per-user preferences, such as the database to use
148 and where to install packages."""
149
150 def __init__(self,
151 flavorOrder=None,
152 downloadDir=None,
153 buildDir=None,
154 installDir=None,
155 pimpDatabase=None):
156 if not flavorOrder:
157 flavorOrder = DEFAULT_FLAVORORDER
158 if not downloadDir:
159 downloadDir = DEFAULT_DOWNLOADDIR
160 if not buildDir:
161 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000162 if not pimpDatabase:
163 pimpDatabase = DEFAULT_PIMPDATABASE
Jack Jansen20fa6752003-04-16 12:15:34 +0000164 self.setInstallDir(installDir)
165 self.flavorOrder = flavorOrder
166 self.downloadDir = downloadDir
167 self.buildDir = buildDir
168 self.pimpDatabase = pimpDatabase
169
170 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000171 if installDir:
172 # Installing to non-standard location.
173 self.installLocations = [
174 ('--install-lib', installDir),
175 ('--install-headers', None),
176 ('--install-scripts', None),
177 ('--install-data', None)]
178 else:
179 installDir = DEFAULT_INSTALLDIR
180 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000181 self.installDir = installDir
Jack Jansen20fa6752003-04-16 12:15:34 +0000182
Jack Jansen0ae32202003-04-09 13:25:43 +0000183 def check(self):
184 """Check that the preferences make sense: directories exist and are
185 writable, the install directory is on sys.path, etc."""
186
187 rv = ""
188 RWX_OK = os.R_OK|os.W_OK|os.X_OK
189 if not os.path.exists(self.downloadDir):
190 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
191 elif not os.access(self.downloadDir, RWX_OK):
192 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
193 if not os.path.exists(self.buildDir):
194 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
195 elif not os.access(self.buildDir, RWX_OK):
196 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
197 if not os.path.exists(self.installDir):
198 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
199 elif not os.access(self.installDir, RWX_OK):
200 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
201 else:
202 installDir = os.path.realpath(self.installDir)
203 for p in sys.path:
204 try:
205 realpath = os.path.realpath(p)
206 except:
207 pass
208 if installDir == realpath:
209 break
210 else:
211 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000212 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000213
214 def compareFlavors(self, left, right):
215 """Compare two flavor strings. This is part of your preferences
216 because whether the user prefers installing from source or binary is."""
217 if left in self.flavorOrder:
218 if right in self.flavorOrder:
219 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
220 return -1
221 if right in self.flavorOrder:
222 return 1
223 return cmp(left, right)
224
Jack Jansen95839b82003-02-09 23:10:20 +0000225class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000226 """Class representing a pimp database. It can actually contain
227 information from multiple databases through inclusion, but the
228 toplevel database is considered the master, as its maintainer is
229 "responsible" for the contents."""
230
231 def __init__(self, prefs):
232 self._packages = []
233 self.preferences = prefs
234 self._urllist = []
235 self._version = ""
236 self._maintainer = ""
237 self._description = ""
238
239 def close(self):
240 """Clean up"""
241 self._packages = []
242 self.preferences = None
243
244 def appendURL(self, url, included=0):
245 """Append packages from the database with the given URL.
246 Only the first database should specify included=0, so the
247 global information (maintainer, description) get stored."""
248
249 if url in self._urllist:
250 return
251 self._urllist.append(url)
252 fp = urllib2.urlopen(url).fp
253 dict = plistlib.Plist.fromFile(fp)
254 # Test here for Pimp version, etc
255 if not included:
256 self._version = dict.get('Version', '0.1')
257 if self._version != PIMP_VERSION:
258 sys.stderr.write("Warning: database version %s does not match %s\n"
259 % (self._version, PIMP_VERSION))
260 self._maintainer = dict.get('Maintainer', '')
261 self._description = dict.get('Description', '')
262 self._appendPackages(dict['Packages'])
263 others = dict.get('Include', [])
264 for url in others:
265 self.appendURL(url, included=1)
266
267 def _appendPackages(self, packages):
268 """Given a list of dictionaries containing package
269 descriptions create the PimpPackage objects and append them
270 to our internal storage."""
271
272 for p in packages:
273 p = dict(p)
274 flavor = p.get('Flavor')
275 if flavor == 'source':
276 pkg = PimpPackage_source(self, p)
277 elif flavor == 'binary':
278 pkg = PimpPackage_binary(self, p)
279 else:
280 pkg = PimpPackage(self, dict(p))
281 self._packages.append(pkg)
282
283 def list(self):
284 """Return a list of all PimpPackage objects in the database."""
285
286 return self._packages
287
288 def listnames(self):
289 """Return a list of names of all packages in the database."""
290
291 rv = []
292 for pkg in self._packages:
293 rv.append(pkg.fullname())
294 rv.sort()
295 return rv
296
297 def dump(self, pathOrFile):
298 """Dump the contents of the database to an XML .plist file.
299
300 The file can be passed as either a file object or a pathname.
301 All data, including included databases, is dumped."""
302
303 packages = []
304 for pkg in self._packages:
305 packages.append(pkg.dump())
306 dict = {
307 'Version': self._version,
308 'Maintainer': self._maintainer,
309 'Description': self._description,
310 'Packages': packages
311 }
312 plist = plistlib.Plist(**dict)
313 plist.write(pathOrFile)
314
315 def find(self, ident):
316 """Find a package. The package can be specified by name
317 or as a dictionary with name, version and flavor entries.
318
319 Only name is obligatory. If there are multiple matches the
320 best one (higher version number, flavors ordered according to
321 users' preference) is returned."""
322
323 if type(ident) == str:
324 # Remove ( and ) for pseudo-packages
325 if ident[0] == '(' and ident[-1] == ')':
326 ident = ident[1:-1]
327 # Split into name-version-flavor
328 fields = ident.split('-')
329 if len(fields) < 1 or len(fields) > 3:
330 return None
331 name = fields[0]
332 if len(fields) > 1:
333 version = fields[1]
334 else:
335 version = None
336 if len(fields) > 2:
337 flavor = fields[2]
338 else:
339 flavor = None
340 else:
341 name = ident['Name']
342 version = ident.get('Version')
343 flavor = ident.get('Flavor')
344 found = None
345 for p in self._packages:
346 if name == p.name() and \
347 (not version or version == p.version()) and \
348 (not flavor or flavor == p.flavor()):
349 if not found or found < p:
350 found = p
351 return found
352
Jack Jansene7b33db2003-02-11 22:40:59 +0000353ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000354 "Name",
355 "Version",
356 "Flavor",
357 "Description",
358 "Home-page",
359 "Download-URL",
360 "Install-test",
361 "Install-command",
362 "Pre-install-command",
363 "Post-install-command",
364 "Prerequisites",
365 "MD5Sum"
Jack Jansene7b33db2003-02-11 22:40:59 +0000366]
367
Jack Jansen95839b82003-02-09 23:10:20 +0000368class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000369 """Class representing a single package."""
370
371 def __init__(self, db, dict):
372 self._db = db
373 name = dict["Name"]
374 for k in dict.keys():
375 if not k in ALLOWED_KEYS:
376 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
377 self._dict = dict
378
379 def __getitem__(self, key):
380 return self._dict[key]
381
382 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000383 def version(self): return self._dict.get('Version')
384 def flavor(self): return self._dict.get('Flavor')
Jack Jansen0ae32202003-04-09 13:25:43 +0000385 def description(self): return self._dict['Description']
386 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000387 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen0ae32202003-04-09 13:25:43 +0000388
389 def fullname(self):
390 """Return the full name "name-version-flavor" of a package.
391
392 If the package is a pseudo-package, something that cannot be
393 installed through pimp, return the name in (parentheses)."""
394
395 rv = self._dict['Name']
396 if self._dict.has_key('Version'):
397 rv = rv + '-%s' % self._dict['Version']
398 if self._dict.has_key('Flavor'):
399 rv = rv + '-%s' % self._dict['Flavor']
400 if not self._dict.get('Download-URL'):
401 # Pseudo-package, show in parentheses
402 rv = '(%s)' % rv
403 return rv
404
405 def dump(self):
406 """Return a dict object containing the information on the package."""
407 return self._dict
408
409 def __cmp__(self, other):
410 """Compare two packages, where the "better" package sorts lower."""
411
412 if not isinstance(other, PimpPackage):
413 return cmp(id(self), id(other))
414 if self.name() != other.name():
415 return cmp(self.name(), other.name())
416 if self.version() != other.version():
417 return -cmp(self.version(), other.version())
418 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
419
420 def installed(self):
421 """Test wheter the package is installed.
422
423 Returns two values: a status indicator which is one of
424 "yes", "no", "old" (an older version is installed) or "bad"
425 (something went wrong during the install test) and a human
426 readable string which may contain more details."""
427
428 namespace = {
429 "NotInstalled": _scriptExc_NotInstalled,
430 "OldInstalled": _scriptExc_OldInstalled,
431 "BadInstalled": _scriptExc_BadInstalled,
432 "os": os,
433 "sys": sys,
434 }
435 installTest = self._dict['Install-test'].strip() + '\n'
436 try:
437 exec installTest in namespace
438 except ImportError, arg:
439 return "no", str(arg)
440 except _scriptExc_NotInstalled, arg:
441 return "no", str(arg)
442 except _scriptExc_OldInstalled, arg:
443 return "old", str(arg)
444 except _scriptExc_BadInstalled, arg:
445 return "bad", str(arg)
446 except:
447 sys.stderr.write("-------------------------------------\n")
448 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
449 sys.stderr.write("---- source:\n")
450 sys.stderr.write(installTest)
451 sys.stderr.write("---- exception:\n")
452 import traceback
453 traceback.print_exc(file=sys.stderr)
454 if self._db._maintainer:
455 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
456 sys.stderr.write("-------------------------------------\n")
457 return "bad", "Package install test got exception"
458 return "yes", ""
459
460 def prerequisites(self):
461 """Return a list of prerequisites for this package.
462
463 The list contains 2-tuples, of which the first item is either
464 a PimpPackage object or None, and the second is a descriptive
465 string. The first item can be None if this package depends on
466 something that isn't pimp-installable, in which case the descriptive
467 string should tell the user what to do."""
468
469 rv = []
470 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000471 # For pseudo-packages that are already installed we don't
472 # return an error message
473 status, _ = self.installed()
474 if status == "yes":
475 return []
Jack Jansen0ae32202003-04-09 13:25:43 +0000476 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000477 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000478 self.fullname())]
479 if not self._dict.get('Prerequisites'):
480 return []
481 for item in self._dict['Prerequisites']:
482 if type(item) == str:
483 pkg = None
484 descr = str(item)
485 else:
486 name = item['Name']
487 if item.has_key('Version'):
488 name = name + '-' + item['Version']
489 if item.has_key('Flavor'):
490 name = name + '-' + item['Flavor']
491 pkg = self._db.find(name)
492 if not pkg:
493 descr = "Requires unknown %s"%name
494 else:
495 descr = pkg.description()
496 rv.append((pkg, descr))
497 return rv
498
Jack Jansen0ae32202003-04-09 13:25:43 +0000499
500 def downloadPackageOnly(self, output=None):
501 """Download a single package, if needed.
502
503 An MD5 signature is used to determine whether download is needed,
504 and to test that we actually downloaded what we expected.
505 If output is given it is a file-like object that will receive a log
506 of what happens.
507
508 If anything unforeseen happened the method returns an error message
509 string.
510 """
511
512 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
513 path = urllib.url2pathname(path)
514 filename = os.path.split(path)[1]
515 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
516 if not self._archiveOK():
517 if scheme == 'manual':
518 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000519 if _cmd(output, self._db.preferences.downloadDir,
Jack Jansen0ae32202003-04-09 13:25:43 +0000520 "curl",
521 "--output", self.archiveFilename,
522 self._dict['Download-URL']):
523 return "download command failed"
524 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
525 return "archive not found after download"
526 if not self._archiveOK():
527 return "archive does not have correct MD5 checksum"
528
529 def _archiveOK(self):
530 """Test an archive. It should exist and the MD5 checksum should be correct."""
531
532 if not os.path.exists(self.archiveFilename):
533 return 0
534 if not self._dict.get('MD5Sum'):
535 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
536 return 1
537 data = open(self.archiveFilename, 'rb').read()
538 checksum = md5.new(data).hexdigest()
539 return checksum == self._dict['MD5Sum']
540
541 def unpackPackageOnly(self, output=None):
542 """Unpack a downloaded package archive."""
543
544 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000545 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000546 if filename[-len(ext):] == ext:
547 break
548 else:
549 return "unknown extension for archive file: %s" % filename
550 self.basename = filename[:-len(ext)]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000551 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir)
552 rv = unpacker.unpack(self.archiveFilename, output=output)
553 if rv:
554 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000555
556 def installPackageOnly(self, output=None):
557 """Default install method, to be overridden by subclasses"""
558 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
559 % (self.fullname(), self._dict.get(flavor, ""))
560
561 def installSinglePackage(self, output=None):
562 """Download, unpack and install a single package.
563
564 If output is given it should be a file-like object and it
565 will receive a log of what happened."""
566
567 if not self._dict['Download-URL']:
568 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
569 msg = self.downloadPackageOnly(output)
570 if msg:
571 return "%s: download: %s" % (self.fullname(), msg)
572
573 msg = self.unpackPackageOnly(output)
574 if msg:
575 return "%s: unpack: %s" % (self.fullname(), msg)
576
577 return self.installPackageOnly(output)
578
579 def beforeInstall(self):
580 """Bookkeeping before installation: remember what we have in site-packages"""
581 self._old_contents = os.listdir(self._db.preferences.installDir)
582
583 def afterInstall(self):
584 """Bookkeeping after installation: interpret any new .pth files that have
585 appeared"""
586
587 new_contents = os.listdir(self._db.preferences.installDir)
588 for fn in new_contents:
589 if fn in self._old_contents:
590 continue
591 if fn[-4:] != '.pth':
592 continue
593 fullname = os.path.join(self._db.preferences.installDir, fn)
594 f = open(fullname)
595 for line in f.readlines():
596 if not line:
597 continue
598 if line[0] == '#':
599 continue
600 if line[:6] == 'import':
601 exec line
602 continue
603 if line[-1] == '\n':
604 line = line[:-1]
605 if not os.path.isabs(line):
606 line = os.path.join(self._db.preferences.installDir, line)
607 line = os.path.realpath(line)
608 if not line in sys.path:
609 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000610
Jack Jansen0dacac42003-02-14 14:11:59 +0000611class PimpPackage_binary(PimpPackage):
612
Jack Jansen0ae32202003-04-09 13:25:43 +0000613 def unpackPackageOnly(self, output=None):
614 """We don't unpack binary packages until installing"""
615 pass
616
617 def installPackageOnly(self, output=None):
618 """Install a single source package.
619
620 If output is given it should be a file-like object and it
621 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000622
Jack Jansen0ae32202003-04-09 13:25:43 +0000623 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000624 return "%s: Binary package cannot have Install-command" % self.fullname()
625
626 if self._dict.has_key('Pre-install-command'):
627 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
628 return "pre-install %s: running \"%s\" failed" % \
629 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000630
631 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000632
Jack Jansen0ae32202003-04-09 13:25:43 +0000633 # Install by unpacking
634 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000635 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000636 if filename[-len(ext):] == ext:
637 break
638 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000639 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
640 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000641
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000642 install_renames = []
643 for k, newloc in self._db.preferences.installLocations:
644 if not newloc:
645 continue
646 if k == "--install-lib":
647 oldloc = DEFAULT_INSTALLDIR
648 else:
649 return "%s: Don't know installLocation %s" % (self.fullname(), k)
650 install_renames.append((oldloc, newloc))
651
652 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
653 rv = unpacker.unpack(self.archiveFilename, output=output)
654 if rv:
655 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000656
657 self.afterInstall()
658
659 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000660 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
661 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000662 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000663
Jack Jansen0ae32202003-04-09 13:25:43 +0000664 return None
665
666
Jack Jansen0dacac42003-02-14 14:11:59 +0000667class PimpPackage_source(PimpPackage):
668
Jack Jansen0ae32202003-04-09 13:25:43 +0000669 def unpackPackageOnly(self, output=None):
670 """Unpack a source package and check that setup.py exists"""
671 PimpPackage.unpackPackageOnly(self, output)
672 # Test that a setup script has been create
673 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
674 setupname = os.path.join(self._buildDirname, "setup.py")
675 if not os.path.exists(setupname) and not NO_EXECUTE:
676 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000677
Jack Jansen0ae32202003-04-09 13:25:43 +0000678 def installPackageOnly(self, output=None):
679 """Install a single source package.
680
681 If output is given it should be a file-like object and it
682 will receive a log of what happened."""
683
684 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000685 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000686 return "pre-install %s: running \"%s\" failed" % \
687 (self.fullname(), self._dict['Pre-install-command'])
688
689 self.beforeInstall()
690 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000691 if installcmd and self._install_renames:
692 return "Package has install-command and can only be installed to standard location"
693 # This is the "bit-bucket" for installations: everything we don't
694 # want. After installation we check that it is actually empty
695 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000696 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000697 extra_args = ""
698 for k, v in self._db.preferences.installLocations:
699 if not v:
700 # We don't want these files installed. Send them
701 # to the bit-bucket.
702 if not unwanted_install_dir:
703 unwanted_install_dir = tempfile.mkdtemp()
704 v = unwanted_install_dir
705 extra_args = extra_args + " %s \"%s\"" % (k, v)
706 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
707 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000708 return "install %s: running \"%s\" failed" % \
709 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000710 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
711 unwanted_files = os.listdir(unwanted_install_dir)
712 if unwanted_files:
713 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
714 else:
715 rv = None
716 shutil.rmtree(unwanted_install_dir)
717 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000718
719 self.afterInstall()
720
721 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000722 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000723 return "post-install %s: running \"%s\" failed" % \
724 (self.fullname(), self._dict['Post-install-command'])
725 return None
726
727
Jack Jansen95839b82003-02-09 23:10:20 +0000728class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000729 """Installer engine: computes dependencies and installs
730 packages in the right order."""
731
732 def __init__(self, db):
733 self._todo = []
734 self._db = db
735 self._curtodo = []
736 self._curmessages = []
737
738 def __contains__(self, package):
739 return package in self._todo
740
741 def _addPackages(self, packages):
742 for package in packages:
743 if not package in self._todo:
744 self._todo.insert(0, package)
745
746 def _prepareInstall(self, package, force=0, recursive=1):
747 """Internal routine, recursive engine for prepareInstall.
748
749 Test whether the package is installed and (if not installed
750 or if force==1) prepend it to the temporary todo list and
751 call ourselves recursively on all prerequisites."""
752
753 if not force:
754 status, message = package.installed()
755 if status == "yes":
756 return
757 if package in self._todo or package in self._curtodo:
758 return
759 self._curtodo.insert(0, package)
760 if not recursive:
761 return
762 prereqs = package.prerequisites()
763 for pkg, descr in prereqs:
764 if pkg:
765 self._prepareInstall(pkg, force, recursive)
766 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000767 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000768
769 def prepareInstall(self, package, force=0, recursive=1):
770 """Prepare installation of a package.
771
772 If the package is already installed and force is false nothing
773 is done. If recursive is true prerequisites are installed first.
774
775 Returns a list of packages (to be passed to install) and a list
776 of messages of any problems encountered.
777 """
778
779 self._curtodo = []
780 self._curmessages = []
781 self._prepareInstall(package, force, recursive)
782 rv = self._curtodo, self._curmessages
783 self._curtodo = []
784 self._curmessages = []
785 return rv
786
787 def install(self, packages, output):
788 """Install a list of packages."""
789
790 self._addPackages(packages)
791 status = []
792 for pkg in self._todo:
793 msg = pkg.installSinglePackage(output)
794 if msg:
795 status.append(msg)
796 return status
797
798
799
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000800def _run(mode, verbose, force, args, prefargs):
Jack Jansen0ae32202003-04-09 13:25:43 +0000801 """Engine for the main program"""
802
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000803 prefs = PimpPreferences(**prefargs)
804 rv = prefs.check()
805 if rv:
806 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000807 db = PimpDatabase(prefs)
808 db.appendURL(prefs.pimpDatabase)
809
810 if mode == 'dump':
811 db.dump(sys.stdout)
812 elif mode =='list':
813 if not args:
814 args = db.listnames()
815 print "%-20.20s\t%s" % ("Package", "Description")
816 print
817 for pkgname in args:
818 pkg = db.find(pkgname)
819 if pkg:
820 description = pkg.description()
821 pkgname = pkg.fullname()
822 else:
823 description = 'Error: no such package'
824 print "%-20.20s\t%s" % (pkgname, description)
825 if verbose:
826 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000827 try:
828 print "\tDownload URL:\t", pkg.downloadURL()
829 except KeyError:
830 pass
Jack Jansen0ae32202003-04-09 13:25:43 +0000831 elif mode =='status':
832 if not args:
833 args = db.listnames()
834 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
835 print
836 for pkgname in args:
837 pkg = db.find(pkgname)
838 if pkg:
839 status, msg = pkg.installed()
840 pkgname = pkg.fullname()
841 else:
842 status = 'error'
843 msg = 'No such package'
844 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
845 if verbose and status == "no":
846 prereq = pkg.prerequisites()
847 for pkg, msg in prereq:
848 if not pkg:
849 pkg = ''
850 else:
851 pkg = pkg.fullname()
852 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
853 elif mode == 'install':
854 if not args:
855 print 'Please specify packages to install'
856 sys.exit(1)
857 inst = PimpInstaller(db)
858 for pkgname in args:
859 pkg = db.find(pkgname)
860 if not pkg:
861 print '%s: No such package' % pkgname
862 continue
863 list, messages = inst.prepareInstall(pkg, force)
864 if messages and not force:
865 print "%s: Not installed:" % pkgname
866 for m in messages:
867 print "\t", m
868 else:
869 if verbose:
870 output = sys.stdout
871 else:
872 output = None
873 messages = inst.install(list, output)
874 if messages:
875 print "%s: Not installed:" % pkgname
876 for m in messages:
877 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000878
879def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000880 """Minimal commandline tool to drive pimp."""
881
882 import getopt
883 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000884 print "Usage: pimp [options] -s [package ...] List installed status"
885 print " pimp [options] -l [package ...] Show package information"
886 print " pimp [options] -i package ... Install packages"
887 print " pimp -d Dump database to stdout"
Jack Jansen0ae32202003-04-09 13:25:43 +0000888 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000889 print " -v Verbose"
890 print " -f Force installation"
891 print " -D dir Set destination directory (default: site-packages)"
Jack Jansen0ae32202003-04-09 13:25:43 +0000892 sys.exit(1)
893
894 try:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000895 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:")
Jack Jansen0ae32202003-04-09 13:25:43 +0000896 except getopt.Error:
897 _help()
898 if not opts and not args:
899 _help()
900 mode = None
901 force = 0
902 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000903 prefargs = {}
Jack Jansen0ae32202003-04-09 13:25:43 +0000904 for o, a in opts:
905 if o == '-s':
906 if mode:
907 _help()
908 mode = 'status'
909 if o == '-l':
910 if mode:
911 _help()
912 mode = 'list'
913 if o == '-d':
914 if mode:
915 _help()
916 mode = 'dump'
917 if o == '-i':
918 mode = 'install'
919 if o == '-f':
920 force = 1
921 if o == '-v':
922 verbose = 1
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000923 if o == '-D':
924 prefargs['installDir'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +0000925 if not mode:
926 _help()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000927 _run(mode, verbose, force, args, prefargs)
Jack Jansen0ae32202003-04-09 13:25:43 +0000928
Jack Jansen95839b82003-02-09 23:10:20 +0000929if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +0000930 main()
931
932