blob: 786f40a43f55424ff8e2ebed242b12674304c3b4 [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 Jansen95839b82003-02-09 23:10:20 +000025
Jack Jansen6a600ab2003-02-10 15:55:51 +000026__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main"]
27
Jack Jansen95839b82003-02-09 23:10:20 +000028_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
29_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
30_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
31
32NO_EXECUTE=0
33
Jack Jansene7b33db2003-02-11 22:40:59 +000034PIMP_VERSION="0.1"
35
Jack Jansen0dacac42003-02-14 14:11:59 +000036# Flavors:
37# source: setup-based package
38# binary: tar (or other) archive created with setup.py bdist.
Jack Jansen95839b82003-02-09 23:10:20 +000039DEFAULT_FLAVORORDER=['source', 'binary']
40DEFAULT_DOWNLOADDIR='/tmp'
41DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000042DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansen95839b82003-02-09 23:10:20 +000043DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
44
45ARCHIVE_FORMATS = [
Jack Jansen0ae32202003-04-09 13:25:43 +000046 (".tar.Z", "zcat \"%s\" | tar -xf -"),
47 (".taz", "zcat \"%s\" | tar -xf -"),
48 (".tar.gz", "zcat \"%s\" | tar -xf -"),
49 (".tgz", "zcat \"%s\" | tar -xf -"),
50 (".tar.bz", "bzcat \"%s\" | tar -xf -"),
51 (".zip", "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +000052]
53
54class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +000055 """Container for per-user preferences, such as the database to use
56 and where to install packages."""
57
58 def __init__(self,
59 flavorOrder=None,
60 downloadDir=None,
61 buildDir=None,
62 installDir=None,
63 pimpDatabase=None):
64 if not flavorOrder:
65 flavorOrder = DEFAULT_FLAVORORDER
66 if not downloadDir:
67 downloadDir = DEFAULT_DOWNLOADDIR
68 if not buildDir:
69 buildDir = DEFAULT_BUILDDIR
70 if not installDir:
71 installDir = DEFAULT_INSTALLDIR
72 if not pimpDatabase:
73 pimpDatabase = DEFAULT_PIMPDATABASE
74 self.flavorOrder = flavorOrder
75 self.downloadDir = downloadDir
76 self.buildDir = buildDir
77 self.installDir = installDir
78 self.pimpDatabase = pimpDatabase
79
80 def check(self):
81 """Check that the preferences make sense: directories exist and are
82 writable, the install directory is on sys.path, etc."""
83
84 rv = ""
85 RWX_OK = os.R_OK|os.W_OK|os.X_OK
86 if not os.path.exists(self.downloadDir):
87 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
88 elif not os.access(self.downloadDir, RWX_OK):
89 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
90 if not os.path.exists(self.buildDir):
91 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
92 elif not os.access(self.buildDir, RWX_OK):
93 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
94 if not os.path.exists(self.installDir):
95 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
96 elif not os.access(self.installDir, RWX_OK):
97 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
98 else:
99 installDir = os.path.realpath(self.installDir)
100 for p in sys.path:
101 try:
102 realpath = os.path.realpath(p)
103 except:
104 pass
105 if installDir == realpath:
106 break
107 else:
108 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
109 return rv
110
111 def compareFlavors(self, left, right):
112 """Compare two flavor strings. This is part of your preferences
113 because whether the user prefers installing from source or binary is."""
114 if left in self.flavorOrder:
115 if right in self.flavorOrder:
116 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
117 return -1
118 if right in self.flavorOrder:
119 return 1
120 return cmp(left, right)
121
Jack Jansen95839b82003-02-09 23:10:20 +0000122class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000123 """Class representing a pimp database. It can actually contain
124 information from multiple databases through inclusion, but the
125 toplevel database is considered the master, as its maintainer is
126 "responsible" for the contents."""
127
128 def __init__(self, prefs):
129 self._packages = []
130 self.preferences = prefs
131 self._urllist = []
132 self._version = ""
133 self._maintainer = ""
134 self._description = ""
135
136 def close(self):
137 """Clean up"""
138 self._packages = []
139 self.preferences = None
140
141 def appendURL(self, url, included=0):
142 """Append packages from the database with the given URL.
143 Only the first database should specify included=0, so the
144 global information (maintainer, description) get stored."""
145
146 if url in self._urllist:
147 return
148 self._urllist.append(url)
149 fp = urllib2.urlopen(url).fp
150 dict = plistlib.Plist.fromFile(fp)
151 # Test here for Pimp version, etc
152 if not included:
153 self._version = dict.get('Version', '0.1')
154 if self._version != PIMP_VERSION:
155 sys.stderr.write("Warning: database version %s does not match %s\n"
156 % (self._version, PIMP_VERSION))
157 self._maintainer = dict.get('Maintainer', '')
158 self._description = dict.get('Description', '')
159 self._appendPackages(dict['Packages'])
160 others = dict.get('Include', [])
161 for url in others:
162 self.appendURL(url, included=1)
163
164 def _appendPackages(self, packages):
165 """Given a list of dictionaries containing package
166 descriptions create the PimpPackage objects and append them
167 to our internal storage."""
168
169 for p in packages:
170 p = dict(p)
171 flavor = p.get('Flavor')
172 if flavor == 'source':
173 pkg = PimpPackage_source(self, p)
174 elif flavor == 'binary':
175 pkg = PimpPackage_binary(self, p)
176 else:
177 pkg = PimpPackage(self, dict(p))
178 self._packages.append(pkg)
179
180 def list(self):
181 """Return a list of all PimpPackage objects in the database."""
182
183 return self._packages
184
185 def listnames(self):
186 """Return a list of names of all packages in the database."""
187
188 rv = []
189 for pkg in self._packages:
190 rv.append(pkg.fullname())
191 rv.sort()
192 return rv
193
194 def dump(self, pathOrFile):
195 """Dump the contents of the database to an XML .plist file.
196
197 The file can be passed as either a file object or a pathname.
198 All data, including included databases, is dumped."""
199
200 packages = []
201 for pkg in self._packages:
202 packages.append(pkg.dump())
203 dict = {
204 'Version': self._version,
205 'Maintainer': self._maintainer,
206 'Description': self._description,
207 'Packages': packages
208 }
209 plist = plistlib.Plist(**dict)
210 plist.write(pathOrFile)
211
212 def find(self, ident):
213 """Find a package. The package can be specified by name
214 or as a dictionary with name, version and flavor entries.
215
216 Only name is obligatory. If there are multiple matches the
217 best one (higher version number, flavors ordered according to
218 users' preference) is returned."""
219
220 if type(ident) == str:
221 # Remove ( and ) for pseudo-packages
222 if ident[0] == '(' and ident[-1] == ')':
223 ident = ident[1:-1]
224 # Split into name-version-flavor
225 fields = ident.split('-')
226 if len(fields) < 1 or len(fields) > 3:
227 return None
228 name = fields[0]
229 if len(fields) > 1:
230 version = fields[1]
231 else:
232 version = None
233 if len(fields) > 2:
234 flavor = fields[2]
235 else:
236 flavor = None
237 else:
238 name = ident['Name']
239 version = ident.get('Version')
240 flavor = ident.get('Flavor')
241 found = None
242 for p in self._packages:
243 if name == p.name() and \
244 (not version or version == p.version()) and \
245 (not flavor or flavor == p.flavor()):
246 if not found or found < p:
247 found = p
248 return found
249
Jack Jansene7b33db2003-02-11 22:40:59 +0000250ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000251 "Name",
252 "Version",
253 "Flavor",
254 "Description",
255 "Home-page",
256 "Download-URL",
257 "Install-test",
258 "Install-command",
259 "Pre-install-command",
260 "Post-install-command",
261 "Prerequisites",
262 "MD5Sum"
Jack Jansene7b33db2003-02-11 22:40:59 +0000263]
264
Jack Jansen95839b82003-02-09 23:10:20 +0000265class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000266 """Class representing a single package."""
267
268 def __init__(self, db, dict):
269 self._db = db
270 name = dict["Name"]
271 for k in dict.keys():
272 if not k in ALLOWED_KEYS:
273 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
274 self._dict = dict
275
276 def __getitem__(self, key):
277 return self._dict[key]
278
279 def name(self): return self._dict['Name']
280 def version(self): return self._dict['Version']
281 def flavor(self): return self._dict['Flavor']
282 def description(self): return self._dict['Description']
283 def homepage(self): return self._dict.get('Home-page')
284 def downloadURL(self): return self._dict['Download-URL']
285
286 def fullname(self):
287 """Return the full name "name-version-flavor" of a package.
288
289 If the package is a pseudo-package, something that cannot be
290 installed through pimp, return the name in (parentheses)."""
291
292 rv = self._dict['Name']
293 if self._dict.has_key('Version'):
294 rv = rv + '-%s' % self._dict['Version']
295 if self._dict.has_key('Flavor'):
296 rv = rv + '-%s' % self._dict['Flavor']
297 if not self._dict.get('Download-URL'):
298 # Pseudo-package, show in parentheses
299 rv = '(%s)' % rv
300 return rv
301
302 def dump(self):
303 """Return a dict object containing the information on the package."""
304 return self._dict
305
306 def __cmp__(self, other):
307 """Compare two packages, where the "better" package sorts lower."""
308
309 if not isinstance(other, PimpPackage):
310 return cmp(id(self), id(other))
311 if self.name() != other.name():
312 return cmp(self.name(), other.name())
313 if self.version() != other.version():
314 return -cmp(self.version(), other.version())
315 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
316
317 def installed(self):
318 """Test wheter the package is installed.
319
320 Returns two values: a status indicator which is one of
321 "yes", "no", "old" (an older version is installed) or "bad"
322 (something went wrong during the install test) and a human
323 readable string which may contain more details."""
324
325 namespace = {
326 "NotInstalled": _scriptExc_NotInstalled,
327 "OldInstalled": _scriptExc_OldInstalled,
328 "BadInstalled": _scriptExc_BadInstalled,
329 "os": os,
330 "sys": sys,
331 }
332 installTest = self._dict['Install-test'].strip() + '\n'
333 try:
334 exec installTest in namespace
335 except ImportError, arg:
336 return "no", str(arg)
337 except _scriptExc_NotInstalled, arg:
338 return "no", str(arg)
339 except _scriptExc_OldInstalled, arg:
340 return "old", str(arg)
341 except _scriptExc_BadInstalled, arg:
342 return "bad", str(arg)
343 except:
344 sys.stderr.write("-------------------------------------\n")
345 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
346 sys.stderr.write("---- source:\n")
347 sys.stderr.write(installTest)
348 sys.stderr.write("---- exception:\n")
349 import traceback
350 traceback.print_exc(file=sys.stderr)
351 if self._db._maintainer:
352 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
353 sys.stderr.write("-------------------------------------\n")
354 return "bad", "Package install test got exception"
355 return "yes", ""
356
357 def prerequisites(self):
358 """Return a list of prerequisites for this package.
359
360 The list contains 2-tuples, of which the first item is either
361 a PimpPackage object or None, and the second is a descriptive
362 string. The first item can be None if this package depends on
363 something that isn't pimp-installable, in which case the descriptive
364 string should tell the user what to do."""
365
366 rv = []
367 if not self._dict.get('Download-URL'):
368 return [(None,
369 "%s: This package needs to be installed manually (no Download-URL field)" %
370 self.fullname())]
371 if not self._dict.get('Prerequisites'):
372 return []
373 for item in self._dict['Prerequisites']:
374 if type(item) == str:
375 pkg = None
376 descr = str(item)
377 else:
378 name = item['Name']
379 if item.has_key('Version'):
380 name = name + '-' + item['Version']
381 if item.has_key('Flavor'):
382 name = name + '-' + item['Flavor']
383 pkg = self._db.find(name)
384 if not pkg:
385 descr = "Requires unknown %s"%name
386 else:
387 descr = pkg.description()
388 rv.append((pkg, descr))
389 return rv
390
391 def _cmd(self, output, dir, *cmditems):
392 """Internal routine to run a shell command in a given directory."""
393
394 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
395 if output:
396 output.write("+ %s\n" % cmd)
397 if NO_EXECUTE:
398 return 0
399 child = popen2.Popen4(cmd)
400 child.tochild.close()
401 while 1:
402 line = child.fromchild.readline()
403 if not line:
404 break
405 if output:
406 output.write(line)
407 return child.wait()
408
409 def downloadPackageOnly(self, output=None):
410 """Download a single package, if needed.
411
412 An MD5 signature is used to determine whether download is needed,
413 and to test that we actually downloaded what we expected.
414 If output is given it is a file-like object that will receive a log
415 of what happens.
416
417 If anything unforeseen happened the method returns an error message
418 string.
419 """
420
421 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
422 path = urllib.url2pathname(path)
423 filename = os.path.split(path)[1]
424 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
425 if not self._archiveOK():
426 if scheme == 'manual':
427 return "Please download package manually and save as %s" % self.archiveFilename
428 if self._cmd(output, self._db.preferences.downloadDir,
429 "curl",
430 "--output", self.archiveFilename,
431 self._dict['Download-URL']):
432 return "download command failed"
433 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
434 return "archive not found after download"
435 if not self._archiveOK():
436 return "archive does not have correct MD5 checksum"
437
438 def _archiveOK(self):
439 """Test an archive. It should exist and the MD5 checksum should be correct."""
440
441 if not os.path.exists(self.archiveFilename):
442 return 0
443 if not self._dict.get('MD5Sum'):
444 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
445 return 1
446 data = open(self.archiveFilename, 'rb').read()
447 checksum = md5.new(data).hexdigest()
448 return checksum == self._dict['MD5Sum']
449
450 def unpackPackageOnly(self, output=None):
451 """Unpack a downloaded package archive."""
452
453 filename = os.path.split(self.archiveFilename)[1]
454 for ext, cmd in ARCHIVE_FORMATS:
455 if filename[-len(ext):] == ext:
456 break
457 else:
458 return "unknown extension for archive file: %s" % filename
459 self.basename = filename[:-len(ext)]
460 cmd = cmd % self.archiveFilename
461 if self._cmd(output, self._db.preferences.buildDir, cmd):
462 return "unpack command failed"
463
464 def installPackageOnly(self, output=None):
465 """Default install method, to be overridden by subclasses"""
466 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
467 % (self.fullname(), self._dict.get(flavor, ""))
468
469 def installSinglePackage(self, output=None):
470 """Download, unpack and install a single package.
471
472 If output is given it should be a file-like object and it
473 will receive a log of what happened."""
474
475 if not self._dict['Download-URL']:
476 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
477 msg = self.downloadPackageOnly(output)
478 if msg:
479 return "%s: download: %s" % (self.fullname(), msg)
480
481 msg = self.unpackPackageOnly(output)
482 if msg:
483 return "%s: unpack: %s" % (self.fullname(), msg)
484
485 return self.installPackageOnly(output)
486
487 def beforeInstall(self):
488 """Bookkeeping before installation: remember what we have in site-packages"""
489 self._old_contents = os.listdir(self._db.preferences.installDir)
490
491 def afterInstall(self):
492 """Bookkeeping after installation: interpret any new .pth files that have
493 appeared"""
494
495 new_contents = os.listdir(self._db.preferences.installDir)
496 for fn in new_contents:
497 if fn in self._old_contents:
498 continue
499 if fn[-4:] != '.pth':
500 continue
501 fullname = os.path.join(self._db.preferences.installDir, fn)
502 f = open(fullname)
503 for line in f.readlines():
504 if not line:
505 continue
506 if line[0] == '#':
507 continue
508 if line[:6] == 'import':
509 exec line
510 continue
511 if line[-1] == '\n':
512 line = line[:-1]
513 if not os.path.isabs(line):
514 line = os.path.join(self._db.preferences.installDir, line)
515 line = os.path.realpath(line)
516 if not line in sys.path:
517 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000518
Jack Jansen0dacac42003-02-14 14:11:59 +0000519class PimpPackage_binary(PimpPackage):
520
Jack Jansen0ae32202003-04-09 13:25:43 +0000521 def unpackPackageOnly(self, output=None):
522 """We don't unpack binary packages until installing"""
523 pass
524
525 def installPackageOnly(self, output=None):
526 """Install a single source package.
527
528 If output is given it should be a file-like object and it
529 will receive a log of what happened."""
530 print 'PimpPackage_binary installPackageOnly'
531
532 msgs = []
533 if self._dict.has_key('Pre-install-command'):
534 msg.append("%s: Pre-install-command ignored" % self.fullname())
535 if self._dict.has_key('Install-command'):
536 msgs.append("%s: Install-command ignored" % self.fullname())
537 if self._dict.has_key('Post-install-command'):
538 msgs.append("%s: Post-install-command ignored" % self.fullname())
539
540 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000541
Jack Jansen0ae32202003-04-09 13:25:43 +0000542 # Install by unpacking
543 filename = os.path.split(self.archiveFilename)[1]
544 for ext, cmd in ARCHIVE_FORMATS:
545 if filename[-len(ext):] == ext:
546 break
547 else:
548 return "unknown extension for archive file: %s" % filename
549
550 # Extract the files in the root folder.
551 cmd = cmd % self.archiveFilename
552 if self._cmd(output, "/", cmd):
553 return "unpack command failed"
554
555 self.afterInstall()
556
557 if self._dict.has_key('Post-install-command'):
558 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
559 return "post-install %s: running \"%s\" failed" % \
560 (self.fullname(), self._dict['Post-install-command'])
561 return None
562
563
Jack Jansen0dacac42003-02-14 14:11:59 +0000564class PimpPackage_source(PimpPackage):
565
Jack Jansen0ae32202003-04-09 13:25:43 +0000566 def unpackPackageOnly(self, output=None):
567 """Unpack a source package and check that setup.py exists"""
568 PimpPackage.unpackPackageOnly(self, output)
569 # Test that a setup script has been create
570 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
571 setupname = os.path.join(self._buildDirname, "setup.py")
572 if not os.path.exists(setupname) and not NO_EXECUTE:
573 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000574
Jack Jansen0ae32202003-04-09 13:25:43 +0000575 def installPackageOnly(self, output=None):
576 """Install a single source package.
577
578 If output is given it should be a file-like object and it
579 will receive a log of what happened."""
580
581 if self._dict.has_key('Pre-install-command'):
582 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
583 return "pre-install %s: running \"%s\" failed" % \
584 (self.fullname(), self._dict['Pre-install-command'])
585
586 self.beforeInstall()
587 installcmd = self._dict.get('Install-command')
588 if not installcmd:
589 installcmd = '"%s" setup.py install' % sys.executable
590 if self._cmd(output, self._buildDirname, installcmd):
591 return "install %s: running \"%s\" failed" % \
592 (self.fullname(), installcmd)
593
594 self.afterInstall()
595
596 if self._dict.has_key('Post-install-command'):
597 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
598 return "post-install %s: running \"%s\" failed" % \
599 (self.fullname(), self._dict['Post-install-command'])
600 return None
601
602
Jack Jansen95839b82003-02-09 23:10:20 +0000603class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000604 """Installer engine: computes dependencies and installs
605 packages in the right order."""
606
607 def __init__(self, db):
608 self._todo = []
609 self._db = db
610 self._curtodo = []
611 self._curmessages = []
612
613 def __contains__(self, package):
614 return package in self._todo
615
616 def _addPackages(self, packages):
617 for package in packages:
618 if not package in self._todo:
619 self._todo.insert(0, package)
620
621 def _prepareInstall(self, package, force=0, recursive=1):
622 """Internal routine, recursive engine for prepareInstall.
623
624 Test whether the package is installed and (if not installed
625 or if force==1) prepend it to the temporary todo list and
626 call ourselves recursively on all prerequisites."""
627
628 if not force:
629 status, message = package.installed()
630 if status == "yes":
631 return
632 if package in self._todo or package in self._curtodo:
633 return
634 self._curtodo.insert(0, package)
635 if not recursive:
636 return
637 prereqs = package.prerequisites()
638 for pkg, descr in prereqs:
639 if pkg:
640 self._prepareInstall(pkg, force, recursive)
641 else:
642 self._curmessages.append("Requires: %s" % descr)
643
644 def prepareInstall(self, package, force=0, recursive=1):
645 """Prepare installation of a package.
646
647 If the package is already installed and force is false nothing
648 is done. If recursive is true prerequisites are installed first.
649
650 Returns a list of packages (to be passed to install) and a list
651 of messages of any problems encountered.
652 """
653
654 self._curtodo = []
655 self._curmessages = []
656 self._prepareInstall(package, force, recursive)
657 rv = self._curtodo, self._curmessages
658 self._curtodo = []
659 self._curmessages = []
660 return rv
661
662 def install(self, packages, output):
663 """Install a list of packages."""
664
665 self._addPackages(packages)
666 status = []
667 for pkg in self._todo:
668 msg = pkg.installSinglePackage(output)
669 if msg:
670 status.append(msg)
671 return status
672
673
674
Jack Jansen95839b82003-02-09 23:10:20 +0000675def _run(mode, verbose, force, args):
Jack Jansen0ae32202003-04-09 13:25:43 +0000676 """Engine for the main program"""
677
678 prefs = PimpPreferences()
679 prefs.check()
680 db = PimpDatabase(prefs)
681 db.appendURL(prefs.pimpDatabase)
682
683 if mode == 'dump':
684 db.dump(sys.stdout)
685 elif mode =='list':
686 if not args:
687 args = db.listnames()
688 print "%-20.20s\t%s" % ("Package", "Description")
689 print
690 for pkgname in args:
691 pkg = db.find(pkgname)
692 if pkg:
693 description = pkg.description()
694 pkgname = pkg.fullname()
695 else:
696 description = 'Error: no such package'
697 print "%-20.20s\t%s" % (pkgname, description)
698 if verbose:
699 print "\tHome page:\t", pkg.homepage()
700 print "\tDownload URL:\t", pkg.downloadURL()
701 elif mode =='status':
702 if not args:
703 args = db.listnames()
704 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
705 print
706 for pkgname in args:
707 pkg = db.find(pkgname)
708 if pkg:
709 status, msg = pkg.installed()
710 pkgname = pkg.fullname()
711 else:
712 status = 'error'
713 msg = 'No such package'
714 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
715 if verbose and status == "no":
716 prereq = pkg.prerequisites()
717 for pkg, msg in prereq:
718 if not pkg:
719 pkg = ''
720 else:
721 pkg = pkg.fullname()
722 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
723 elif mode == 'install':
724 if not args:
725 print 'Please specify packages to install'
726 sys.exit(1)
727 inst = PimpInstaller(db)
728 for pkgname in args:
729 pkg = db.find(pkgname)
730 if not pkg:
731 print '%s: No such package' % pkgname
732 continue
733 list, messages = inst.prepareInstall(pkg, force)
734 if messages and not force:
735 print "%s: Not installed:" % pkgname
736 for m in messages:
737 print "\t", m
738 else:
739 if verbose:
740 output = sys.stdout
741 else:
742 output = None
743 messages = inst.install(list, output)
744 if messages:
745 print "%s: Not installed:" % pkgname
746 for m in messages:
747 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +0000748
749def main():
Jack Jansen0ae32202003-04-09 13:25:43 +0000750 """Minimal commandline tool to drive pimp."""
751
752 import getopt
753 def _help():
754 print "Usage: pimp [-v] -s [package ...] List installed status"
755 print " pimp [-v] -l [package ...] Show package information"
756 print " pimp [-vf] -i package ... Install packages"
757 print " pimp -d Dump database to stdout"
758 print "Options:"
759 print " -v Verbose"
760 print " -f Force installation"
761 sys.exit(1)
762
763 try:
764 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
765 except getopt.Error:
766 _help()
767 if not opts and not args:
768 _help()
769 mode = None
770 force = 0
771 verbose = 0
772 for o, a in opts:
773 if o == '-s':
774 if mode:
775 _help()
776 mode = 'status'
777 if o == '-l':
778 if mode:
779 _help()
780 mode = 'list'
781 if o == '-d':
782 if mode:
783 _help()
784 mode = 'dump'
785 if o == '-i':
786 mode = 'install'
787 if o == '-f':
788 force = 1
789 if o == '-v':
790 verbose = 1
791 if not mode:
792 _help()
793 _run(mode, verbose, force, args)
794
Jack Jansen95839b82003-02-09 23:10:20 +0000795if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +0000796 main()
797
798