blob: 8a44963360fe7b2479ffcca4d231b47597799e95 [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 Jansen95839b82003-02-09 23:10:20 +000046DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
47
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:
135 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']
383 def version(self): return self._dict['Version']
384 def flavor(self): return self._dict['Flavor']
385 def description(self): return self._dict['Description']
386 def homepage(self): return self._dict.get('Home-page')
387 def downloadURL(self): return self._dict['Download-URL']
388
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'):
471 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000472 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000473 self.fullname())]
474 if not self._dict.get('Prerequisites'):
475 return []
476 for item in self._dict['Prerequisites']:
477 if type(item) == str:
478 pkg = None
479 descr = str(item)
480 else:
481 name = item['Name']
482 if item.has_key('Version'):
483 name = name + '-' + item['Version']
484 if item.has_key('Flavor'):
485 name = name + '-' + item['Flavor']
486 pkg = self._db.find(name)
487 if not pkg:
488 descr = "Requires unknown %s"%name
489 else:
490 descr = pkg.description()
491 rv.append((pkg, descr))
492 return rv
493
Jack Jansen0ae32202003-04-09 13:25:43 +0000494
495 def downloadPackageOnly(self, output=None):
496 """Download a single package, if needed.
497
498 An MD5 signature is used to determine whether download is needed,
499 and to test that we actually downloaded what we expected.
500 If output is given it is a file-like object that will receive a log
501 of what happens.
502
503 If anything unforeseen happened the method returns an error message
504 string.
505 """
506
507 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
508 path = urllib.url2pathname(path)
509 filename = os.path.split(path)[1]
510 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
511 if not self._archiveOK():
512 if scheme == 'manual':
513 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000514 if _cmd(output, self._db.preferences.downloadDir,
Jack Jansen0ae32202003-04-09 13:25:43 +0000515 "curl",
516 "--output", self.archiveFilename,
517 self._dict['Download-URL']):
518 return "download command failed"
519 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
520 return "archive not found after download"
521 if not self._archiveOK():
522 return "archive does not have correct MD5 checksum"
523
524 def _archiveOK(self):
525 """Test an archive. It should exist and the MD5 checksum should be correct."""
526
527 if not os.path.exists(self.archiveFilename):
528 return 0
529 if not self._dict.get('MD5Sum'):
530 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
531 return 1
532 data = open(self.archiveFilename, 'rb').read()
533 checksum = md5.new(data).hexdigest()
534 return checksum == self._dict['MD5Sum']
535
536 def unpackPackageOnly(self, output=None):
537 """Unpack a downloaded package archive."""
538
539 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000540 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000541 if filename[-len(ext):] == ext:
542 break
543 else:
544 return "unknown extension for archive file: %s" % filename
545 self.basename = filename[:-len(ext)]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000546 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir)
547 rv = unpacker.unpack(self.archiveFilename, output=output)
548 if rv:
549 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000550
551 def installPackageOnly(self, output=None):
552 """Default install method, to be overridden by subclasses"""
553 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
554 % (self.fullname(), self._dict.get(flavor, ""))
555
556 def installSinglePackage(self, output=None):
557 """Download, unpack and install a single package.
558
559 If output is given it should be a file-like object and it
560 will receive a log of what happened."""
561
562 if not self._dict['Download-URL']:
563 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
564 msg = self.downloadPackageOnly(output)
565 if msg:
566 return "%s: download: %s" % (self.fullname(), msg)
567
568 msg = self.unpackPackageOnly(output)
569 if msg:
570 return "%s: unpack: %s" % (self.fullname(), msg)
571
572 return self.installPackageOnly(output)
573
574 def beforeInstall(self):
575 """Bookkeeping before installation: remember what we have in site-packages"""
576 self._old_contents = os.listdir(self._db.preferences.installDir)
577
578 def afterInstall(self):
579 """Bookkeeping after installation: interpret any new .pth files that have
580 appeared"""
581
582 new_contents = os.listdir(self._db.preferences.installDir)
583 for fn in new_contents:
584 if fn in self._old_contents:
585 continue
586 if fn[-4:] != '.pth':
587 continue
588 fullname = os.path.join(self._db.preferences.installDir, fn)
589 f = open(fullname)
590 for line in f.readlines():
591 if not line:
592 continue
593 if line[0] == '#':
594 continue
595 if line[:6] == 'import':
596 exec line
597 continue
598 if line[-1] == '\n':
599 line = line[:-1]
600 if not os.path.isabs(line):
601 line = os.path.join(self._db.preferences.installDir, line)
602 line = os.path.realpath(line)
603 if not line in sys.path:
604 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000605
Jack Jansen0dacac42003-02-14 14:11:59 +0000606class PimpPackage_binary(PimpPackage):
607
Jack Jansen0ae32202003-04-09 13:25:43 +0000608 def unpackPackageOnly(self, output=None):
609 """We don't unpack binary packages until installing"""
610 pass
611
612 def installPackageOnly(self, output=None):
613 """Install a single source package.
614
615 If output is given it should be a file-like object and it
616 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000617
Jack Jansen0ae32202003-04-09 13:25:43 +0000618 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000619 return "%s: Binary package cannot have Install-command" % self.fullname()
620
621 if self._dict.has_key('Pre-install-command'):
622 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
623 return "pre-install %s: running \"%s\" failed" % \
624 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000625
626 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000627
Jack Jansen0ae32202003-04-09 13:25:43 +0000628 # Install by unpacking
629 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000630 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000631 if filename[-len(ext):] == ext:
632 break
633 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000634 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
635 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000636
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000637 install_renames = []
638 for k, newloc in self._db.preferences.installLocations:
639 if not newloc:
640 continue
641 if k == "--install-lib":
642 oldloc = DEFAULT_INSTALLDIR
643 else:
644 return "%s: Don't know installLocation %s" % (self.fullname(), k)
645 install_renames.append((oldloc, newloc))
646
647 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
648 rv = unpacker.unpack(self.archiveFilename, output=output)
649 if rv:
650 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000651
652 self.afterInstall()
653
654 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000655 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
656 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000657 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000658
Jack Jansen0ae32202003-04-09 13:25:43 +0000659 return None
660
661
Jack Jansen0dacac42003-02-14 14:11:59 +0000662class PimpPackage_source(PimpPackage):
663
Jack Jansen0ae32202003-04-09 13:25:43 +0000664 def unpackPackageOnly(self, output=None):
665 """Unpack a source package and check that setup.py exists"""
666 PimpPackage.unpackPackageOnly(self, output)
667 # Test that a setup script has been create
668 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
669 setupname = os.path.join(self._buildDirname, "setup.py")
670 if not os.path.exists(setupname) and not NO_EXECUTE:
671 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000672
Jack Jansen0ae32202003-04-09 13:25:43 +0000673 def installPackageOnly(self, output=None):
674 """Install a single source package.
675
676 If output is given it should be a file-like object and it
677 will receive a log of what happened."""
678
679 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000680 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000681 return "pre-install %s: running \"%s\" failed" % \
682 (self.fullname(), self._dict['Pre-install-command'])
683
684 self.beforeInstall()
685 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000686 if installcmd and self._install_renames:
687 return "Package has install-command and can only be installed to standard location"
688 # This is the "bit-bucket" for installations: everything we don't
689 # want. After installation we check that it is actually empty
690 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000691 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000692 extra_args = ""
693 for k, v in self._db.preferences.installLocations:
694 if not v:
695 # We don't want these files installed. Send them
696 # to the bit-bucket.
697 if not unwanted_install_dir:
698 unwanted_install_dir = tempfile.mkdtemp()
699 v = unwanted_install_dir
700 extra_args = extra_args + " %s \"%s\"" % (k, v)
701 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
702 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000703 return "install %s: running \"%s\" failed" % \
704 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000705 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
706 unwanted_files = os.listdir(unwanted_install_dir)
707 if unwanted_files:
708 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
709 else:
710 rv = None
711 shutil.rmtree(unwanted_install_dir)
712 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000713
714 self.afterInstall()
715
716 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000717 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000718 return "post-install %s: running \"%s\" failed" % \
719 (self.fullname(), self._dict['Post-install-command'])
720 return None
721
722
Jack Jansen95839b82003-02-09 23:10:20 +0000723class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000724 """Installer engine: computes dependencies and installs
725 packages in the right order."""
726
727 def __init__(self, db):
728 self._todo = []
729 self._db = db
730 self._curtodo = []
731 self._curmessages = []
732
733 def __contains__(self, package):
734 return package in self._todo
735
736 def _addPackages(self, packages):
737 for package in packages:
738 if not package in self._todo:
739 self._todo.insert(0, package)
740
741 def _prepareInstall(self, package, force=0, recursive=1):
742 """Internal routine, recursive engine for prepareInstall.
743
744 Test whether the package is installed and (if not installed
745 or if force==1) prepend it to the temporary todo list and
746 call ourselves recursively on all prerequisites."""
747
748 if not force:
749 status, message = package.installed()
750 if status == "yes":
751 return
752 if package in self._todo or package in self._curtodo:
753 return
754 self._curtodo.insert(0, package)
755 if not recursive:
756 return
757 prereqs = package.prerequisites()
758 for pkg, descr in prereqs:
759 if pkg:
760 self._prepareInstall(pkg, force, recursive)
761 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000762 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000763
764 def prepareInstall(self, package, force=0, recursive=1):
765 """Prepare installation of a package.
766
767 If the package is already installed and force is false nothing
768 is done. If recursive is true prerequisites are installed first.
769
770 Returns a list of packages (to be passed to install) and a list
771 of messages of any problems encountered.
772 """
773
774 self._curtodo = []
775 self._curmessages = []
776 self._prepareInstall(package, force, recursive)
777 rv = self._curtodo, self._curmessages
778 self._curtodo = []
779 self._curmessages = []
780 return rv
781
782 def install(self, packages, output):
783 """Install a list of packages."""
784
785 self._addPackages(packages)
786 status = []
787 for pkg in self._todo:
788 msg = pkg.installSinglePackage(output)
789 if msg:
790 status.append(msg)
791 return status
792
793
794
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000795def _run(mode, verbose, force, args, prefargs):
Jack Jansen0ae32202003-04-09 13:25:43 +0000796 """Engine for the main program"""
797
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000798 prefs = PimpPreferences(**prefargs)
799 rv = prefs.check()
800 if rv:
801 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000802 db = PimpDatabase(prefs)
803 db.appendURL(prefs.pimpDatabase)
804
805 if mode == 'dump':
806 db.dump(sys.stdout)
807 elif mode =='list':
808 if not args:
809 args = db.listnames()
810 print "%-20.20s\t%s" % ("Package", "Description")
811 print
812 for pkgname in args:
813 pkg = db.find(pkgname)
814 if pkg:
815 description = pkg.description()
816 pkgname = pkg.fullname()
817 else:
818 description = 'Error: no such package'
819 print "%-20.20s\t%s" % (pkgname, description)
820 if verbose:
821 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000822 try:
823 print "\tDownload URL:\t", pkg.downloadURL()
824 except KeyError:
825 pass
Jack Jansen0ae32202003-04-09 13:25:43 +0000826 elif mode =='status':
827 if not args:
828 args = db.listnames()
829 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
830 print
831 for pkgname in args:
832 pkg = db.find(pkgname)
833 if pkg:
834 status, msg = pkg.installed()
835 pkgname = pkg.fullname()
836 else:
837 status = 'error'
838 msg = 'No such package'
839 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
840 if verbose and status == "no":
841 prereq = pkg.prerequisites()
842 for pkg, msg in prereq:
843 if not pkg:
844 pkg = ''
845 else:
846 pkg = pkg.fullname()
847 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
848 elif mode == 'install':
849 if not args:
850 print 'Please specify packages to install'
851 sys.exit(1)
852 inst = PimpInstaller(db)
853 for pkgname in args:
854 pkg = db.find(pkgname)
855 if not pkg:
856 print '%s: No such package' % pkgname
857 continue
858 list, messages = inst.prepareInstall(pkg, force)
859 if messages and not force:
860 print "%s: Not installed:" % pkgname
861 for m in messages:
862 print "\t", m
863 else:
864 if verbose:
865 output = sys.stdout
866 else:
867 output = None
868 messages = inst.install(list, output)
869 if messages:
870 print "%s: Not installed:" % pkgname
871 for m in messages:
872 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000873
874def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000875 """Minimal commandline tool to drive pimp."""
876
877 import getopt
878 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000879 print "Usage: pimp [options] -s [package ...] List installed status"
880 print " pimp [options] -l [package ...] Show package information"
881 print " pimp [options] -i package ... Install packages"
882 print " pimp -d Dump database to stdout"
Jack Jansen0ae32202003-04-09 13:25:43 +0000883 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000884 print " -v Verbose"
885 print " -f Force installation"
886 print " -D dir Set destination directory (default: site-packages)"
Jack Jansen0ae32202003-04-09 13:25:43 +0000887 sys.exit(1)
888
889 try:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000890 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:")
Jack Jansen0ae32202003-04-09 13:25:43 +0000891 except getopt.Error:
892 _help()
893 if not opts and not args:
894 _help()
895 mode = None
896 force = 0
897 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000898 prefargs = {}
Jack Jansen0ae32202003-04-09 13:25:43 +0000899 for o, a in opts:
900 if o == '-s':
901 if mode:
902 _help()
903 mode = 'status'
904 if o == '-l':
905 if mode:
906 _help()
907 mode = 'list'
908 if o == '-d':
909 if mode:
910 _help()
911 mode = 'dump'
912 if o == '-i':
913 mode = 'install'
914 if o == '-f':
915 force = 1
916 if o == '-v':
917 verbose = 1
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000918 if o == '-D':
919 prefargs['installDir'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +0000920 if not mode:
921 _help()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000922 _run(mode, verbose, force, args, prefargs)
Jack Jansen0ae32202003-04-09 13:25:43 +0000923
Jack Jansen95839b82003-02-09 23:10:20 +0000924if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +0000925 main()
926
927