blob: b3bf781c5dc19d3aef4bd189be1e6a861407cb4b [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] != '/']
134 return "Not all files were unpacked: %s" % " ".join(names)
135
Jack Jansen95839b82003-02-09 23:10:20 +0000136ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000137 (".tar.Z", PimpTarUnpacker, None),
138 (".taz", PimpTarUnpacker, None),
139 (".tar.gz", PimpTarUnpacker, None),
140 (".tgz", PimpTarUnpacker, None),
141 (".tar.bz", PimpTarUnpacker, None),
142 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000143]
144
145class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000146 """Container for per-user preferences, such as the database to use
147 and where to install packages."""
148
149 def __init__(self,
150 flavorOrder=None,
151 downloadDir=None,
152 buildDir=None,
153 installDir=None,
154 pimpDatabase=None):
155 if not flavorOrder:
156 flavorOrder = DEFAULT_FLAVORORDER
157 if not downloadDir:
158 downloadDir = DEFAULT_DOWNLOADDIR
159 if not buildDir:
160 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000161 if not pimpDatabase:
162 pimpDatabase = DEFAULT_PIMPDATABASE
Jack Jansen20fa6752003-04-16 12:15:34 +0000163 self.setInstallDir(installDir)
164 self.flavorOrder = flavorOrder
165 self.downloadDir = downloadDir
166 self.buildDir = buildDir
167 self.pimpDatabase = pimpDatabase
168
169 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000170 if installDir:
171 # Installing to non-standard location.
172 self.installLocations = [
173 ('--install-lib', installDir),
174 ('--install-headers', None),
175 ('--install-scripts', None),
176 ('--install-data', None)]
177 else:
178 installDir = DEFAULT_INSTALLDIR
179 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000180 self.installDir = installDir
Jack Jansen20fa6752003-04-16 12:15:34 +0000181
Jack Jansen0ae32202003-04-09 13:25:43 +0000182 def check(self):
183 """Check that the preferences make sense: directories exist and are
184 writable, the install directory is on sys.path, etc."""
185
186 rv = ""
187 RWX_OK = os.R_OK|os.W_OK|os.X_OK
188 if not os.path.exists(self.downloadDir):
189 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
190 elif not os.access(self.downloadDir, RWX_OK):
191 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
192 if not os.path.exists(self.buildDir):
193 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
194 elif not os.access(self.buildDir, RWX_OK):
195 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
196 if not os.path.exists(self.installDir):
197 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
198 elif not os.access(self.installDir, RWX_OK):
199 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
200 else:
201 installDir = os.path.realpath(self.installDir)
202 for p in sys.path:
203 try:
204 realpath = os.path.realpath(p)
205 except:
206 pass
207 if installDir == realpath:
208 break
209 else:
210 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000211 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000212
213 def compareFlavors(self, left, right):
214 """Compare two flavor strings. This is part of your preferences
215 because whether the user prefers installing from source or binary is."""
216 if left in self.flavorOrder:
217 if right in self.flavorOrder:
218 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
219 return -1
220 if right in self.flavorOrder:
221 return 1
222 return cmp(left, right)
223
Jack Jansen95839b82003-02-09 23:10:20 +0000224class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000225 """Class representing a pimp database. It can actually contain
226 information from multiple databases through inclusion, but the
227 toplevel database is considered the master, as its maintainer is
228 "responsible" for the contents."""
229
230 def __init__(self, prefs):
231 self._packages = []
232 self.preferences = prefs
233 self._urllist = []
234 self._version = ""
235 self._maintainer = ""
236 self._description = ""
237
238 def close(self):
239 """Clean up"""
240 self._packages = []
241 self.preferences = None
242
243 def appendURL(self, url, included=0):
244 """Append packages from the database with the given URL.
245 Only the first database should specify included=0, so the
246 global information (maintainer, description) get stored."""
247
248 if url in self._urllist:
249 return
250 self._urllist.append(url)
251 fp = urllib2.urlopen(url).fp
252 dict = plistlib.Plist.fromFile(fp)
253 # Test here for Pimp version, etc
254 if not included:
255 self._version = dict.get('Version', '0.1')
256 if self._version != PIMP_VERSION:
257 sys.stderr.write("Warning: database version %s does not match %s\n"
258 % (self._version, PIMP_VERSION))
259 self._maintainer = dict.get('Maintainer', '')
260 self._description = dict.get('Description', '')
261 self._appendPackages(dict['Packages'])
262 others = dict.get('Include', [])
263 for url in others:
264 self.appendURL(url, included=1)
265
266 def _appendPackages(self, packages):
267 """Given a list of dictionaries containing package
268 descriptions create the PimpPackage objects and append them
269 to our internal storage."""
270
271 for p in packages:
272 p = dict(p)
273 flavor = p.get('Flavor')
274 if flavor == 'source':
275 pkg = PimpPackage_source(self, p)
276 elif flavor == 'binary':
277 pkg = PimpPackage_binary(self, p)
278 else:
279 pkg = PimpPackage(self, dict(p))
280 self._packages.append(pkg)
281
282 def list(self):
283 """Return a list of all PimpPackage objects in the database."""
284
285 return self._packages
286
287 def listnames(self):
288 """Return a list of names of all packages in the database."""
289
290 rv = []
291 for pkg in self._packages:
292 rv.append(pkg.fullname())
293 rv.sort()
294 return rv
295
296 def dump(self, pathOrFile):
297 """Dump the contents of the database to an XML .plist file.
298
299 The file can be passed as either a file object or a pathname.
300 All data, including included databases, is dumped."""
301
302 packages = []
303 for pkg in self._packages:
304 packages.append(pkg.dump())
305 dict = {
306 'Version': self._version,
307 'Maintainer': self._maintainer,
308 'Description': self._description,
309 'Packages': packages
310 }
311 plist = plistlib.Plist(**dict)
312 plist.write(pathOrFile)
313
314 def find(self, ident):
315 """Find a package. The package can be specified by name
316 or as a dictionary with name, version and flavor entries.
317
318 Only name is obligatory. If there are multiple matches the
319 best one (higher version number, flavors ordered according to
320 users' preference) is returned."""
321
322 if type(ident) == str:
323 # Remove ( and ) for pseudo-packages
324 if ident[0] == '(' and ident[-1] == ')':
325 ident = ident[1:-1]
326 # Split into name-version-flavor
327 fields = ident.split('-')
328 if len(fields) < 1 or len(fields) > 3:
329 return None
330 name = fields[0]
331 if len(fields) > 1:
332 version = fields[1]
333 else:
334 version = None
335 if len(fields) > 2:
336 flavor = fields[2]
337 else:
338 flavor = None
339 else:
340 name = ident['Name']
341 version = ident.get('Version')
342 flavor = ident.get('Flavor')
343 found = None
344 for p in self._packages:
345 if name == p.name() and \
346 (not version or version == p.version()) and \
347 (not flavor or flavor == p.flavor()):
348 if not found or found < p:
349 found = p
350 return found
351
Jack Jansene7b33db2003-02-11 22:40:59 +0000352ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000353 "Name",
354 "Version",
355 "Flavor",
356 "Description",
357 "Home-page",
358 "Download-URL",
359 "Install-test",
360 "Install-command",
361 "Pre-install-command",
362 "Post-install-command",
363 "Prerequisites",
364 "MD5Sum"
Jack Jansene7b33db2003-02-11 22:40:59 +0000365]
366
Jack Jansen95839b82003-02-09 23:10:20 +0000367class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000368 """Class representing a single package."""
369
370 def __init__(self, db, dict):
371 self._db = db
372 name = dict["Name"]
373 for k in dict.keys():
374 if not k in ALLOWED_KEYS:
375 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
376 self._dict = dict
377
378 def __getitem__(self, key):
379 return self._dict[key]
380
381 def name(self): return self._dict['Name']
382 def version(self): return self._dict['Version']
383 def flavor(self): return self._dict['Flavor']
384 def description(self): return self._dict['Description']
385 def homepage(self): return self._dict.get('Home-page')
386 def downloadURL(self): return self._dict['Download-URL']
387
388 def fullname(self):
389 """Return the full name "name-version-flavor" of a package.
390
391 If the package is a pseudo-package, something that cannot be
392 installed through pimp, return the name in (parentheses)."""
393
394 rv = self._dict['Name']
395 if self._dict.has_key('Version'):
396 rv = rv + '-%s' % self._dict['Version']
397 if self._dict.has_key('Flavor'):
398 rv = rv + '-%s' % self._dict['Flavor']
399 if not self._dict.get('Download-URL'):
400 # Pseudo-package, show in parentheses
401 rv = '(%s)' % rv
402 return rv
403
404 def dump(self):
405 """Return a dict object containing the information on the package."""
406 return self._dict
407
408 def __cmp__(self, other):
409 """Compare two packages, where the "better" package sorts lower."""
410
411 if not isinstance(other, PimpPackage):
412 return cmp(id(self), id(other))
413 if self.name() != other.name():
414 return cmp(self.name(), other.name())
415 if self.version() != other.version():
416 return -cmp(self.version(), other.version())
417 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
418
419 def installed(self):
420 """Test wheter the package is installed.
421
422 Returns two values: a status indicator which is one of
423 "yes", "no", "old" (an older version is installed) or "bad"
424 (something went wrong during the install test) and a human
425 readable string which may contain more details."""
426
427 namespace = {
428 "NotInstalled": _scriptExc_NotInstalled,
429 "OldInstalled": _scriptExc_OldInstalled,
430 "BadInstalled": _scriptExc_BadInstalled,
431 "os": os,
432 "sys": sys,
433 }
434 installTest = self._dict['Install-test'].strip() + '\n'
435 try:
436 exec installTest in namespace
437 except ImportError, arg:
438 return "no", str(arg)
439 except _scriptExc_NotInstalled, arg:
440 return "no", str(arg)
441 except _scriptExc_OldInstalled, arg:
442 return "old", str(arg)
443 except _scriptExc_BadInstalled, arg:
444 return "bad", str(arg)
445 except:
446 sys.stderr.write("-------------------------------------\n")
447 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
448 sys.stderr.write("---- source:\n")
449 sys.stderr.write(installTest)
450 sys.stderr.write("---- exception:\n")
451 import traceback
452 traceback.print_exc(file=sys.stderr)
453 if self._db._maintainer:
454 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
455 sys.stderr.write("-------------------------------------\n")
456 return "bad", "Package install test got exception"
457 return "yes", ""
458
459 def prerequisites(self):
460 """Return a list of prerequisites for this package.
461
462 The list contains 2-tuples, of which the first item is either
463 a PimpPackage object or None, and the second is a descriptive
464 string. The first item can be None if this package depends on
465 something that isn't pimp-installable, in which case the descriptive
466 string should tell the user what to do."""
467
468 rv = []
469 if not self._dict.get('Download-URL'):
470 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000471 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000472 self.fullname())]
473 if not self._dict.get('Prerequisites'):
474 return []
475 for item in self._dict['Prerequisites']:
476 if type(item) == str:
477 pkg = None
478 descr = str(item)
479 else:
480 name = item['Name']
481 if item.has_key('Version'):
482 name = name + '-' + item['Version']
483 if item.has_key('Flavor'):
484 name = name + '-' + item['Flavor']
485 pkg = self._db.find(name)
486 if not pkg:
487 descr = "Requires unknown %s"%name
488 else:
489 descr = pkg.description()
490 rv.append((pkg, descr))
491 return rv
492
Jack Jansen0ae32202003-04-09 13:25:43 +0000493
494 def downloadPackageOnly(self, output=None):
495 """Download a single package, if needed.
496
497 An MD5 signature is used to determine whether download is needed,
498 and to test that we actually downloaded what we expected.
499 If output is given it is a file-like object that will receive a log
500 of what happens.
501
502 If anything unforeseen happened the method returns an error message
503 string.
504 """
505
506 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
507 path = urllib.url2pathname(path)
508 filename = os.path.split(path)[1]
509 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
510 if not self._archiveOK():
511 if scheme == 'manual':
512 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000513 if _cmd(output, self._db.preferences.downloadDir,
Jack Jansen0ae32202003-04-09 13:25:43 +0000514 "curl",
515 "--output", self.archiveFilename,
516 self._dict['Download-URL']):
517 return "download command failed"
518 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
519 return "archive not found after download"
520 if not self._archiveOK():
521 return "archive does not have correct MD5 checksum"
522
523 def _archiveOK(self):
524 """Test an archive. It should exist and the MD5 checksum should be correct."""
525
526 if not os.path.exists(self.archiveFilename):
527 return 0
528 if not self._dict.get('MD5Sum'):
529 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
530 return 1
531 data = open(self.archiveFilename, 'rb').read()
532 checksum = md5.new(data).hexdigest()
533 return checksum == self._dict['MD5Sum']
534
535 def unpackPackageOnly(self, output=None):
536 """Unpack a downloaded package archive."""
537
538 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000539 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000540 if filename[-len(ext):] == ext:
541 break
542 else:
543 return "unknown extension for archive file: %s" % filename
544 self.basename = filename[:-len(ext)]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000545 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir)
546 rv = unpacker.unpack(self.archiveFilename, output=output)
547 if rv:
548 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000549
550 def installPackageOnly(self, output=None):
551 """Default install method, to be overridden by subclasses"""
552 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
553 % (self.fullname(), self._dict.get(flavor, ""))
554
555 def installSinglePackage(self, output=None):
556 """Download, unpack and install a single package.
557
558 If output is given it should be a file-like object and it
559 will receive a log of what happened."""
560
561 if not self._dict['Download-URL']:
562 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
563 msg = self.downloadPackageOnly(output)
564 if msg:
565 return "%s: download: %s" % (self.fullname(), msg)
566
567 msg = self.unpackPackageOnly(output)
568 if msg:
569 return "%s: unpack: %s" % (self.fullname(), msg)
570
571 return self.installPackageOnly(output)
572
573 def beforeInstall(self):
574 """Bookkeeping before installation: remember what we have in site-packages"""
575 self._old_contents = os.listdir(self._db.preferences.installDir)
576
577 def afterInstall(self):
578 """Bookkeeping after installation: interpret any new .pth files that have
579 appeared"""
580
581 new_contents = os.listdir(self._db.preferences.installDir)
582 for fn in new_contents:
583 if fn in self._old_contents:
584 continue
585 if fn[-4:] != '.pth':
586 continue
587 fullname = os.path.join(self._db.preferences.installDir, fn)
588 f = open(fullname)
589 for line in f.readlines():
590 if not line:
591 continue
592 if line[0] == '#':
593 continue
594 if line[:6] == 'import':
595 exec line
596 continue
597 if line[-1] == '\n':
598 line = line[:-1]
599 if not os.path.isabs(line):
600 line = os.path.join(self._db.preferences.installDir, line)
601 line = os.path.realpath(line)
602 if not line in sys.path:
603 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000604
Jack Jansen0dacac42003-02-14 14:11:59 +0000605class PimpPackage_binary(PimpPackage):
606
Jack Jansen0ae32202003-04-09 13:25:43 +0000607 def unpackPackageOnly(self, output=None):
608 """We don't unpack binary packages until installing"""
609 pass
610
611 def installPackageOnly(self, output=None):
612 """Install a single source package.
613
614 If output is given it should be a file-like object and it
615 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000616
Jack Jansen0ae32202003-04-09 13:25:43 +0000617 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000618 return "%s: Binary package cannot have Install-command" % self.fullname()
619
620 if self._dict.has_key('Pre-install-command'):
621 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
622 return "pre-install %s: running \"%s\" failed" % \
623 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000624
625 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000626
Jack Jansen0ae32202003-04-09 13:25:43 +0000627 # Install by unpacking
628 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000629 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000630 if filename[-len(ext):] == ext:
631 break
632 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000633 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
634 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000635
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000636 install_renames = []
637 for k, newloc in self._db.preferences.installLocations:
638 if not newloc:
639 continue
640 if k == "--install-lib":
641 oldloc = DEFAULT_INSTALLDIR
642 else:
643 return "%s: Don't know installLocation %s" % (self.fullname(), k)
644 install_renames.append((oldloc, newloc))
645
646 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
647 rv = unpacker.unpack(self.archiveFilename, output=output)
648 if rv:
649 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000650
651 self.afterInstall()
652
653 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000654 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
655 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000656 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000657
Jack Jansen0ae32202003-04-09 13:25:43 +0000658 return None
659
660
Jack Jansen0dacac42003-02-14 14:11:59 +0000661class PimpPackage_source(PimpPackage):
662
Jack Jansen0ae32202003-04-09 13:25:43 +0000663 def unpackPackageOnly(self, output=None):
664 """Unpack a source package and check that setup.py exists"""
665 PimpPackage.unpackPackageOnly(self, output)
666 # Test that a setup script has been create
667 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
668 setupname = os.path.join(self._buildDirname, "setup.py")
669 if not os.path.exists(setupname) and not NO_EXECUTE:
670 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000671
Jack Jansen0ae32202003-04-09 13:25:43 +0000672 def installPackageOnly(self, output=None):
673 """Install a single source package.
674
675 If output is given it should be a file-like object and it
676 will receive a log of what happened."""
677
678 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000679 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000680 return "pre-install %s: running \"%s\" failed" % \
681 (self.fullname(), self._dict['Pre-install-command'])
682
683 self.beforeInstall()
684 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000685 if installcmd and self._install_renames:
686 return "Package has install-command and can only be installed to standard location"
687 # This is the "bit-bucket" for installations: everything we don't
688 # want. After installation we check that it is actually empty
689 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000690 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000691 extra_args = ""
692 for k, v in self._db.preferences.installLocations:
693 if not v:
694 # We don't want these files installed. Send them
695 # to the bit-bucket.
696 if not unwanted_install_dir:
697 unwanted_install_dir = tempfile.mkdtemp()
698 v = unwanted_install_dir
699 extra_args = extra_args + " %s \"%s\"" % (k, v)
700 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
701 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000702 return "install %s: running \"%s\" failed" % \
703 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000704 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
705 unwanted_files = os.listdir(unwanted_install_dir)
706 if unwanted_files:
707 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
708 else:
709 rv = None
710 shutil.rmtree(unwanted_install_dir)
711 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000712
713 self.afterInstall()
714
715 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000716 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000717 return "post-install %s: running \"%s\" failed" % \
718 (self.fullname(), self._dict['Post-install-command'])
719 return None
720
721
Jack Jansen95839b82003-02-09 23:10:20 +0000722class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000723 """Installer engine: computes dependencies and installs
724 packages in the right order."""
725
726 def __init__(self, db):
727 self._todo = []
728 self._db = db
729 self._curtodo = []
730 self._curmessages = []
731
732 def __contains__(self, package):
733 return package in self._todo
734
735 def _addPackages(self, packages):
736 for package in packages:
737 if not package in self._todo:
738 self._todo.insert(0, package)
739
740 def _prepareInstall(self, package, force=0, recursive=1):
741 """Internal routine, recursive engine for prepareInstall.
742
743 Test whether the package is installed and (if not installed
744 or if force==1) prepend it to the temporary todo list and
745 call ourselves recursively on all prerequisites."""
746
747 if not force:
748 status, message = package.installed()
749 if status == "yes":
750 return
751 if package in self._todo or package in self._curtodo:
752 return
753 self._curtodo.insert(0, package)
754 if not recursive:
755 return
756 prereqs = package.prerequisites()
757 for pkg, descr in prereqs:
758 if pkg:
759 self._prepareInstall(pkg, force, recursive)
760 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000761 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000762
763 def prepareInstall(self, package, force=0, recursive=1):
764 """Prepare installation of a package.
765
766 If the package is already installed and force is false nothing
767 is done. If recursive is true prerequisites are installed first.
768
769 Returns a list of packages (to be passed to install) and a list
770 of messages of any problems encountered.
771 """
772
773 self._curtodo = []
774 self._curmessages = []
775 self._prepareInstall(package, force, recursive)
776 rv = self._curtodo, self._curmessages
777 self._curtodo = []
778 self._curmessages = []
779 return rv
780
781 def install(self, packages, output):
782 """Install a list of packages."""
783
784 self._addPackages(packages)
785 status = []
786 for pkg in self._todo:
787 msg = pkg.installSinglePackage(output)
788 if msg:
789 status.append(msg)
790 return status
791
792
793
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000794def _run(mode, verbose, force, args, prefargs):
Jack Jansen0ae32202003-04-09 13:25:43 +0000795 """Engine for the main program"""
796
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000797 prefs = PimpPreferences(**prefargs)
798 rv = prefs.check()
799 if rv:
800 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000801 db = PimpDatabase(prefs)
802 db.appendURL(prefs.pimpDatabase)
803
804 if mode == 'dump':
805 db.dump(sys.stdout)
806 elif mode =='list':
807 if not args:
808 args = db.listnames()
809 print "%-20.20s\t%s" % ("Package", "Description")
810 print
811 for pkgname in args:
812 pkg = db.find(pkgname)
813 if pkg:
814 description = pkg.description()
815 pkgname = pkg.fullname()
816 else:
817 description = 'Error: no such package'
818 print "%-20.20s\t%s" % (pkgname, description)
819 if verbose:
820 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000821 try:
822 print "\tDownload URL:\t", pkg.downloadURL()
823 except KeyError:
824 pass
Jack Jansen0ae32202003-04-09 13:25:43 +0000825 elif mode =='status':
826 if not args:
827 args = db.listnames()
828 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
829 print
830 for pkgname in args:
831 pkg = db.find(pkgname)
832 if pkg:
833 status, msg = pkg.installed()
834 pkgname = pkg.fullname()
835 else:
836 status = 'error'
837 msg = 'No such package'
838 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
839 if verbose and status == "no":
840 prereq = pkg.prerequisites()
841 for pkg, msg in prereq:
842 if not pkg:
843 pkg = ''
844 else:
845 pkg = pkg.fullname()
846 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
847 elif mode == 'install':
848 if not args:
849 print 'Please specify packages to install'
850 sys.exit(1)
851 inst = PimpInstaller(db)
852 for pkgname in args:
853 pkg = db.find(pkgname)
854 if not pkg:
855 print '%s: No such package' % pkgname
856 continue
857 list, messages = inst.prepareInstall(pkg, force)
858 if messages and not force:
859 print "%s: Not installed:" % pkgname
860 for m in messages:
861 print "\t", m
862 else:
863 if verbose:
864 output = sys.stdout
865 else:
866 output = None
867 messages = inst.install(list, output)
868 if messages:
869 print "%s: Not installed:" % pkgname
870 for m in messages:
871 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000872
873def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000874 """Minimal commandline tool to drive pimp."""
875
876 import getopt
877 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000878 print "Usage: pimp [options] -s [package ...] List installed status"
879 print " pimp [options] -l [package ...] Show package information"
880 print " pimp [options] -i package ... Install packages"
881 print " pimp -d Dump database to stdout"
Jack Jansen0ae32202003-04-09 13:25:43 +0000882 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000883 print " -v Verbose"
884 print " -f Force installation"
885 print " -D dir Set destination directory (default: site-packages)"
Jack Jansen0ae32202003-04-09 13:25:43 +0000886 sys.exit(1)
887
888 try:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000889 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:")
Jack Jansen0ae32202003-04-09 13:25:43 +0000890 except getopt.Error:
891 _help()
892 if not opts and not args:
893 _help()
894 mode = None
895 force = 0
896 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000897 prefargs = {}
Jack Jansen0ae32202003-04-09 13:25:43 +0000898 for o, a in opts:
899 if o == '-s':
900 if mode:
901 _help()
902 mode = 'status'
903 if o == '-l':
904 if mode:
905 _help()
906 mode = 'list'
907 if o == '-d':
908 if mode:
909 _help()
910 mode = 'dump'
911 if o == '-i':
912 mode = 'install'
913 if o == '-f':
914 force = 1
915 if o == '-v':
916 verbose = 1
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000917 if o == '-D':
918 prefargs['installDir'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +0000919 if not mode:
920 _help()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000921 _run(mode, verbose, force, args, prefargs)
Jack Jansen0ae32202003-04-09 13:25:43 +0000922
Jack Jansen95839b82003-02-09 23:10:20 +0000923if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +0000924 main()
925
926