blob: 61a99264173f0e53ff0d2ae2dc92b5ab44d89fdf [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
17import urllib
18import urlparse
19import plistlib
20import distutils.util
Jack Jansenc4b217d2003-02-10 13:38:44 +000021import md5
Jack Jansen95839b82003-02-09 23:10:20 +000022
Jack Jansen6a600ab2003-02-10 15:55:51 +000023__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main"]
24
Jack Jansen95839b82003-02-09 23:10:20 +000025_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
26_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
27_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
28
29NO_EXECUTE=0
30
Jack Jansene7b33db2003-02-11 22:40:59 +000031PIMP_VERSION="0.1"
32
Jack Jansen95839b82003-02-09 23:10:20 +000033DEFAULT_FLAVORORDER=['source', 'binary']
34DEFAULT_DOWNLOADDIR='/tmp'
35DEFAULT_BUILDDIR='/tmp'
36DEFAULT_INSTALLDIR=os.path.join(sys.prefix, "Lib", "site-packages")
37DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
38
39ARCHIVE_FORMATS = [
40 (".tar.Z", "zcat \"%s\" | tar xf -"),
41 (".taz", "zcat \"%s\" | tar xf -"),
42 (".tar.gz", "zcat \"%s\" | tar xf -"),
43 (".tgz", "zcat \"%s\" | tar xf -"),
44 (".tar.bz", "bzcat \"%s\" | tar xf -"),
45]
46
Jack Jansenc4b217d2003-02-10 13:38:44 +000047class MyURLopener(urllib.FancyURLopener):
Jack Jansen8d326b82003-02-10 16:08:17 +000048 """Like FancyURLOpener, but we do want to get errors as exceptions."""
Jack Jansenc4b217d2003-02-10 13:38:44 +000049 def http_error_default(self, url, fp, errcode, errmsg, headers):
50 urllib.URLopener.http_error_default(self, url, fp, errcode, errmsg, headers)
51
Jack Jansen95839b82003-02-09 23:10:20 +000052class PimpPreferences:
Jack Jansen6a600ab2003-02-10 15:55:51 +000053 """Container for per-user preferences, such as the database to use
Jack Jansen8d326b82003-02-10 16:08:17 +000054 and where to install packages."""
Jack Jansen6a600ab2003-02-10 15:55:51 +000055
Jack Jansen95839b82003-02-09 23:10:20 +000056 def __init__(self,
57 flavorOrder=None,
58 downloadDir=None,
59 buildDir=None,
60 installDir=None,
61 pimpDatabase=None):
62 if not flavorOrder:
63 flavorOrder = DEFAULT_FLAVORORDER
64 if not downloadDir:
65 downloadDir = DEFAULT_DOWNLOADDIR
66 if not buildDir:
67 buildDir = DEFAULT_BUILDDIR
68 if not installDir:
69 installDir = DEFAULT_INSTALLDIR
70 if not pimpDatabase:
71 pimpDatabase = DEFAULT_PIMPDATABASE
72 self.flavorOrder = flavorOrder
73 self.downloadDir = downloadDir
74 self.buildDir = buildDir
75 self.installDir = installDir
76 self.pimpDatabase = pimpDatabase
77
78 def check(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +000079 """Check that the preferences make sense: directories exist and are
80 writable, the install directory is on sys.path, etc."""
81
Jack Jansen95839b82003-02-09 23:10:20 +000082 rv = ""
83 RWX_OK = os.R_OK|os.W_OK|os.X_OK
84 if not os.path.exists(self.downloadDir):
85 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
86 elif not os.access(self.downloadDir, RWX_OK):
87 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
88 if not os.path.exists(self.buildDir):
89 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
90 elif not os.access(self.buildDir, RWX_OK):
91 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
92 if not os.path.exists(self.installDir):
93 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
94 elif not os.access(self.installDir, RWX_OK):
95 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
96 else:
97 installDir = os.path.realpath(self.installDir)
98 for p in sys.path:
99 try:
100 realpath = os.path.realpath(p)
101 except:
102 pass
103 if installDir == realpath:
104 break
105 else:
106 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
107 return rv
108
109 def compareFlavors(self, left, right):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000110 """Compare two flavor strings. This is part of your preferences
111 because whether the user prefers installing from source or binary is."""
Jack Jansen95839b82003-02-09 23:10:20 +0000112 if left in self.flavorOrder:
113 if right in self.flavorOrder:
114 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
115 return -1
116 if right in self.flavorOrder:
117 return 1
118 return cmp(left, right)
119
120class PimpDatabase:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000121 """Class representing a pimp database. It can actually contain
122 information from multiple databases through inclusion, but the
123 toplevel database is considered the master, as its maintainer is
Jack Jansen8d326b82003-02-10 16:08:17 +0000124 "responsible" for the contents."""
Jack Jansen6a600ab2003-02-10 15:55:51 +0000125
Jack Jansen95839b82003-02-09 23:10:20 +0000126 def __init__(self, prefs):
127 self._packages = []
128 self.preferences = prefs
129 self._urllist = []
130 self._version = ""
131 self._maintainer = ""
132 self._description = ""
133
134 def appendURL(self, url, included=0):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000135 """Append packages from the database with the given URL.
136 Only the first database should specify included=0, so the
137 global information (maintainer, description) get stored."""
138
Jack Jansen95839b82003-02-09 23:10:20 +0000139 if url in self._urllist:
140 return
141 self._urllist.append(url)
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000142 fp = MyURLopener().open(url).fp
Jack Jansen95839b82003-02-09 23:10:20 +0000143 dict = plistlib.Plist.fromFile(fp)
144 # Test here for Pimp version, etc
145 if not included:
Jack Jansene7b33db2003-02-11 22:40:59 +0000146 self._version = dict.get('Version', '0.1')
147 if self._version != PIMP_VERSION:
148 sys.stderr.write("Warning: database version %s does not match %s\n"
149 % (self._version, PIMP_VERSION))
150 self._maintainer = dict.get('Maintainer', '')
151 self._description = dict.get('Description', '')
152 self._appendPackages(dict['Packages'])
153 others = dict.get('Include', [])
Jack Jansen95839b82003-02-09 23:10:20 +0000154 for url in others:
155 self.appendURL(url, included=1)
156
Jack Jansen6a600ab2003-02-10 15:55:51 +0000157 def _appendPackages(self, packages):
158 """Given a list of dictionaries containing package
159 descriptions create the PimpPackage objects and append them
160 to our internal storage."""
161
Jack Jansen95839b82003-02-09 23:10:20 +0000162 for p in packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000163 pkg = PimpPackage(self, dict(p))
Jack Jansen95839b82003-02-09 23:10:20 +0000164 self._packages.append(pkg)
165
166 def list(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000167 """Return a list of all PimpPackage objects in the database."""
168
Jack Jansen95839b82003-02-09 23:10:20 +0000169 return self._packages
170
171 def listnames(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000172 """Return a list of names of all packages in the database."""
173
Jack Jansen95839b82003-02-09 23:10:20 +0000174 rv = []
175 for pkg in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000176 rv.append(pkg.fullname())
Jack Jansen95839b82003-02-09 23:10:20 +0000177 return rv
178
179 def dump(self, pathOrFile):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000180 """Dump the contents of the database to an XML .plist file.
181
182 The file can be passed as either a file object or a pathname.
183 All data, including included databases, is dumped."""
184
Jack Jansen95839b82003-02-09 23:10:20 +0000185 packages = []
186 for pkg in self._packages:
187 packages.append(pkg.dump())
188 dict = {
Jack Jansene7b33db2003-02-11 22:40:59 +0000189 'Version': self._version,
190 'Maintainer': self._maintainer,
191 'Description': self._description,
192 'Packages': packages
Jack Jansen95839b82003-02-09 23:10:20 +0000193 }
194 plist = plistlib.Plist(**dict)
195 plist.write(pathOrFile)
196
197 def find(self, ident):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000198 """Find a package. The package can be specified by name
199 or as a dictionary with name, version and flavor entries.
200
201 Only name is obligatory. If there are multiple matches the
202 best one (higher version number, flavors ordered according to
203 users' preference) is returned."""
204
Jack Jansen95839b82003-02-09 23:10:20 +0000205 if type(ident) == str:
206 # Remove ( and ) for pseudo-packages
207 if ident[0] == '(' and ident[-1] == ')':
208 ident = ident[1:-1]
209 # Split into name-version-flavor
210 fields = ident.split('-')
211 if len(fields) < 1 or len(fields) > 3:
212 return None
213 name = fields[0]
214 if len(fields) > 1:
215 version = fields[1]
216 else:
217 version = None
218 if len(fields) > 2:
219 flavor = fields[2]
220 else:
221 flavor = None
222 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000223 name = ident['Name']
224 version = ident.get('Version')
225 flavor = ident.get('Flavor')
Jack Jansen95839b82003-02-09 23:10:20 +0000226 found = None
227 for p in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000228 if name == p.name() and \
229 (not version or version == p.version()) and \
230 (not flavor or flavor == p.flavor()):
Jack Jansen95839b82003-02-09 23:10:20 +0000231 if not found or found < p:
232 found = p
233 return found
234
Jack Jansene7b33db2003-02-11 22:40:59 +0000235ALLOWED_KEYS = [
236 "Name",
237 "Version",
238 "Flavor",
239 "Description",
240 "Home-page",
241 "Download-URL",
242 "Install-test",
243 "Install-command",
244 "Pre-install-command",
245 "Post-install-command",
246 "Prerequisites",
247 "MD5Sum"
248]
249
Jack Jansen95839b82003-02-09 23:10:20 +0000250class PimpPackage:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000251 """Class representing a single package."""
252
Jack Jansene7b33db2003-02-11 22:40:59 +0000253 def __init__(self, db, dict):
Jack Jansen95839b82003-02-09 23:10:20 +0000254 self._db = db
Jack Jansene7b33db2003-02-11 22:40:59 +0000255 name = dict["Name"]
256 for k in dict.keys():
257 if not k in ALLOWED_KEYS:
258 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
259 self._dict = dict
260
261 def __getitem__(self, key):
262 return self._dict[key]
Jack Jansen95839b82003-02-09 23:10:20 +0000263
Jack Jansene7b33db2003-02-11 22:40:59 +0000264 def name(self): return self._dict['Name']
265 def version(self): return self._dict['Version']
266 def flavor(self): return self._dict['Flavor']
267 def description(self): return self._dict['Description']
268 def homepage(self): return self._dict['Home-page']
269 def downloadURL(self): return self._dict['Download-URL']
270
271 def fullname(self):
272 """Return the full name "name-version-flavor" of a package.
273
274 If the package is a pseudo-package, something that cannot be
275 installed through pimp, return the name in (parentheses)."""
276
277 rv = self._dict['Name']
278 if self._dict.has_key('Version'):
279 rv = rv + '-%s' % self._dict['Version']
280 if self._dict.has_key('Flavor'):
281 rv = rv + '-%s' % self._dict['Flavor']
282 if not self._dict.get('Download-URL'):
283 # Pseudo-package, show in parentheses
284 rv = '(%s)' % rv
285 return rv
286
Jack Jansen95839b82003-02-09 23:10:20 +0000287 def dump(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000288 """Return a dict object containing the information on the package."""
Jack Jansene7b33db2003-02-11 22:40:59 +0000289 return self._dict
Jack Jansen95839b82003-02-09 23:10:20 +0000290
291 def __cmp__(self, other):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000292 """Compare two packages, where the "better" package sorts lower."""
293
Jack Jansen95839b82003-02-09 23:10:20 +0000294 if not isinstance(other, PimpPackage):
295 return cmp(id(self), id(other))
Jack Jansene7b33db2003-02-11 22:40:59 +0000296 if self.name() != other.name():
297 return cmp(self.name(), other.name())
298 if self.version() != other.version():
299 return -cmp(self.version(), other.version())
300 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Jack Jansen95839b82003-02-09 23:10:20 +0000301
302 def installed(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000303 """Test wheter the package is installed.
304
305 Returns two values: a status indicator which is one of
306 "yes", "no", "old" (an older version is installed) or "bad"
307 (something went wrong during the install test) and a human
308 readable string which may contain more details."""
309
Jack Jansen95839b82003-02-09 23:10:20 +0000310 namespace = {
311 "NotInstalled": _scriptExc_NotInstalled,
312 "OldInstalled": _scriptExc_OldInstalled,
313 "BadInstalled": _scriptExc_BadInstalled,
314 "os": os,
315 "sys": sys,
316 }
Jack Jansene7b33db2003-02-11 22:40:59 +0000317 installTest = self._dict['Install-test'].strip() + '\n'
Jack Jansen95839b82003-02-09 23:10:20 +0000318 try:
319 exec installTest in namespace
320 except ImportError, arg:
321 return "no", str(arg)
322 except _scriptExc_NotInstalled, arg:
323 return "no", str(arg)
324 except _scriptExc_OldInstalled, arg:
325 return "old", str(arg)
326 except _scriptExc_BadInstalled, arg:
327 return "bad", str(arg)
328 except:
Jack Jansen95839b82003-02-09 23:10:20 +0000329 return "bad", "Package install test got exception"
330 return "yes", ""
331
332 def prerequisites(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000333 """Return a list of prerequisites for this package.
334
335 The list contains 2-tuples, of which the first item is either
336 a PimpPackage object or None, and the second is a descriptive
337 string. The first item can be None if this package depends on
338 something that isn't pimp-installable, in which case the descriptive
339 string should tell the user what to do."""
340
Jack Jansen95839b82003-02-09 23:10:20 +0000341 rv = []
Jack Jansene7b33db2003-02-11 22:40:59 +0000342 if not self._dict['Download-URL']:
Jack Jansen95839b82003-02-09 23:10:20 +0000343 return [(None, "This package needs to be installed manually")]
Jack Jansene7b33db2003-02-11 22:40:59 +0000344 if not self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000345 return []
Jack Jansene7b33db2003-02-11 22:40:59 +0000346 for item in self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000347 if type(item) == str:
348 pkg = None
349 descr = str(item)
350 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000351 name = item['Name']
352 if item.has_key('Version'):
353 name = name + '-' + item['Version']
354 if item.has_key('Flavor'):
355 name = name + '-' + item['Flavor']
356 pkg = self._db.find(name)
Jack Jansen95839b82003-02-09 23:10:20 +0000357 if not pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000358 descr = "Requires unknown %s"%name
Jack Jansen95839b82003-02-09 23:10:20 +0000359 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000360 descr = pkg.description()
Jack Jansen95839b82003-02-09 23:10:20 +0000361 rv.append((pkg, descr))
362 return rv
363
364 def _cmd(self, output, dir, *cmditems):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000365 """Internal routine to run a shell command in a given directory."""
366
Jack Jansen95839b82003-02-09 23:10:20 +0000367 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
368 if output:
369 output.write("+ %s\n" % cmd)
370 if NO_EXECUTE:
371 return 0
372 fp = os.popen(cmd, "r")
373 while 1:
374 line = fp.readline()
375 if not line:
376 break
377 if output:
378 output.write(line)
379 rv = fp.close()
380 return rv
381
Jack Jansen6a600ab2003-02-10 15:55:51 +0000382 def downloadSinglePackage(self, output=None):
383 """Download a single package, if needed.
384
385 An MD5 signature is used to determine whether download is needed,
386 and to test that we actually downloaded what we expected.
387 If output is given it is a file-like object that will receive a log
388 of what happens.
389
390 If anything unforeseen happened the method returns an error message
391 string.
392 """
393
Jack Jansene7b33db2003-02-11 22:40:59 +0000394 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
Jack Jansen95839b82003-02-09 23:10:20 +0000395 path = urllib.url2pathname(path)
396 filename = os.path.split(path)[1]
Jack Jansenc4b217d2003-02-10 13:38:44 +0000397 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
398 if not self._archiveOK():
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000399 if scheme == 'manual':
400 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansenc4b217d2003-02-10 13:38:44 +0000401 if self._cmd(output, self._db.preferences.downloadDir,
402 "curl",
403 "--output", self.archiveFilename,
Jack Jansene7b33db2003-02-11 22:40:59 +0000404 self._dict['Download-URL']):
Jack Jansenc4b217d2003-02-10 13:38:44 +0000405 return "download command failed"
Jack Jansen95839b82003-02-09 23:10:20 +0000406 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
407 return "archive not found after download"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000408 if not self._archiveOK():
409 return "archive does not have correct MD5 checksum"
410
411 def _archiveOK(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000412 """Test an archive. It should exist and the MD5 checksum should be correct."""
413
Jack Jansenc4b217d2003-02-10 13:38:44 +0000414 if not os.path.exists(self.archiveFilename):
415 return 0
Jack Jansene7b33db2003-02-11 22:40:59 +0000416 if not self._dict['MD5Sum']:
417 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
Jack Jansenc4b217d2003-02-10 13:38:44 +0000418 return 1
419 data = open(self.archiveFilename, 'rb').read()
420 checksum = md5.new(data).hexdigest()
Jack Jansene7b33db2003-02-11 22:40:59 +0000421 return checksum == self._dict['MD5Sum']
Jack Jansen95839b82003-02-09 23:10:20 +0000422
Jack Jansen6a600ab2003-02-10 15:55:51 +0000423 def unpackSinglePackage(self, output=None):
424 """Unpack a downloaded package archive."""
425
Jack Jansen95839b82003-02-09 23:10:20 +0000426 filename = os.path.split(self.archiveFilename)[1]
427 for ext, cmd in ARCHIVE_FORMATS:
428 if filename[-len(ext):] == ext:
429 break
430 else:
431 return "unknown extension for archive file: %s" % filename
432 basename = filename[:-len(ext)]
433 cmd = cmd % self.archiveFilename
434 self._buildDirname = os.path.join(self._db.preferences.buildDir, basename)
435 if self._cmd(output, self._db.preferences.buildDir, cmd):
436 return "unpack command failed"
437 setupname = os.path.join(self._buildDirname, "setup.py")
438 if not os.path.exists(setupname) and not NO_EXECUTE:
439 return "no setup.py found after unpack of archive"
440
Jack Jansen6a600ab2003-02-10 15:55:51 +0000441 def installSinglePackage(self, output=None):
442 """Download, unpack and install a single package.
443
444 If output is given it should be a file-like object and it
445 will receive a log of what happened."""
446
Jack Jansene7b33db2003-02-11 22:40:59 +0000447 if not self._dict['Download-URL']:
Jack Jansen95839b82003-02-09 23:10:20 +0000448 return "%s: This package needs to be installed manually" % _fmtpackagename(self)
449 msg = self.downloadSinglePackage(output)
450 if msg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000451 return "download %s: %s" % (self.fullname(), msg)
Jack Jansen95839b82003-02-09 23:10:20 +0000452 msg = self.unpackSinglePackage(output)
453 if msg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000454 return "unpack %s: %s" % (self.fullname(), msg)
455 if self._dict.has_key('Pre-install-command'):
456 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000457 return "pre-install %s: running \"%s\" failed" % \
Jack Jansene7b33db2003-02-11 22:40:59 +0000458 (self.fullname(), self._dict['Pre-install-command'])
459 installcmd = self._dict.get('Install-command')
460 if not installcmd:
461 installcmd = '"%s" setup.py install' % sys.executable
462 if self._cmd(output, self._buildDirname, installcmd):
463 return "install %s: running \"%s\" failed" % self.fullname()
464 if self._dict.has_key('Post-install-command'):
465 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000466 return "post-install %s: running \"%s\" failed" % \
Jack Jansene7b33db2003-02-11 22:40:59 +0000467 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen95839b82003-02-09 23:10:20 +0000468 return None
469
470class PimpInstaller:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000471 """Installer engine: computes dependencies and installs
472 packages in the right order."""
473
Jack Jansen95839b82003-02-09 23:10:20 +0000474 def __init__(self, db):
475 self._todo = []
476 self._db = db
477 self._curtodo = []
478 self._curmessages = []
479
480 def __contains__(self, package):
481 return package in self._todo
482
483 def _addPackages(self, packages):
484 for package in packages:
485 if not package in self._todo:
486 self._todo.insert(0, package)
487
488 def _prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000489 """Internal routine, recursive engine for prepareInstall.
490
491 Test whether the package is installed and (if not installed
492 or if force==1) prepend it to the temporary todo list and
493 call ourselves recursively on all prerequisites."""
494
Jack Jansen95839b82003-02-09 23:10:20 +0000495 if not force:
496 status, message = package.installed()
497 if status == "yes":
498 return
499 if package in self._todo or package in self._curtodo:
500 return
501 self._curtodo.insert(0, package)
502 if not recursive:
503 return
504 prereqs = package.prerequisites()
505 for pkg, descr in prereqs:
506 if pkg:
507 self._prepareInstall(pkg, force, recursive)
508 else:
509 self._curmessages.append("Requires: %s" % descr)
510
511 def prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000512 """Prepare installation of a package.
513
514 If the package is already installed and force is false nothing
515 is done. If recursive is true prerequisites are installed first.
516
517 Returns a list of packages (to be passed to install) and a list
518 of messages of any problems encountered.
519 """
520
Jack Jansen95839b82003-02-09 23:10:20 +0000521 self._curtodo = []
522 self._curmessages = []
523 self._prepareInstall(package, force, recursive)
524 rv = self._curtodo, self._curmessages
525 self._curtodo = []
526 self._curmessages = []
527 return rv
528
529 def install(self, packages, output):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000530 """Install a list of packages."""
531
Jack Jansen95839b82003-02-09 23:10:20 +0000532 self._addPackages(packages)
533 status = []
534 for pkg in self._todo:
535 msg = pkg.installSinglePackage(output)
536 if msg:
537 status.append(msg)
538 return status
539
540
Jack Jansen95839b82003-02-09 23:10:20 +0000541
542def _run(mode, verbose, force, args):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000543 """Engine for the main program"""
544
Jack Jansen95839b82003-02-09 23:10:20 +0000545 prefs = PimpPreferences()
546 prefs.check()
547 db = PimpDatabase(prefs)
548 db.appendURL(prefs.pimpDatabase)
549
Jack Jansenc4b217d2003-02-10 13:38:44 +0000550 if mode == 'dump':
551 db.dump(sys.stdout)
552 elif mode =='list':
Jack Jansen95839b82003-02-09 23:10:20 +0000553 if not args:
554 args = db.listnames()
555 print "%-20.20s\t%s" % ("Package", "Description")
556 print
557 for pkgname in args:
558 pkg = db.find(pkgname)
559 if pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000560 description = pkg.description()
561 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000562 else:
563 description = 'Error: no such package'
564 print "%-20.20s\t%s" % (pkgname, description)
565 if verbose:
Jack Jansene7b33db2003-02-11 22:40:59 +0000566 print "\tHome page:\t", pkg.homepage()
567 print "\tDownload URL:\t", pkg.downloadURL()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000568 elif mode =='status':
Jack Jansen95839b82003-02-09 23:10:20 +0000569 if not args:
570 args = db.listnames()
571 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
572 print
573 for pkgname in args:
574 pkg = db.find(pkgname)
575 if pkg:
576 status, msg = pkg.installed()
Jack Jansene7b33db2003-02-11 22:40:59 +0000577 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000578 else:
579 status = 'error'
580 msg = 'No such package'
581 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
582 if verbose and status == "no":
583 prereq = pkg.prerequisites()
584 for pkg, msg in prereq:
585 if not pkg:
586 pkg = ''
587 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000588 pkg = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000589 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
590 elif mode == 'install':
591 if not args:
592 print 'Please specify packages to install'
593 sys.exit(1)
594 inst = PimpInstaller(db)
595 for pkgname in args:
596 pkg = db.find(pkgname)
597 if not pkg:
598 print '%s: No such package' % pkgname
599 continue
600 list, messages = inst.prepareInstall(pkg, force)
601 if messages and not force:
602 print "%s: Not installed:" % pkgname
603 for m in messages:
604 print "\t", m
605 else:
606 if verbose:
607 output = sys.stdout
608 else:
609 output = None
610 messages = inst.install(list, output)
611 if messages:
612 print "%s: Not installed:" % pkgname
613 for m in messages:
614 print "\t", m
615
616def main():
Jack Jansen6a600ab2003-02-10 15:55:51 +0000617 """Minimal commandline tool to drive pimp."""
618
Jack Jansen95839b82003-02-09 23:10:20 +0000619 import getopt
620 def _help():
621 print "Usage: pimp [-v] -s [package ...] List installed status"
622 print " pimp [-v] -l [package ...] Show package information"
623 print " pimp [-vf] -i package ... Install packages"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000624 print " pimp -d Dump database to stdout"
Jack Jansen95839b82003-02-09 23:10:20 +0000625 print "Options:"
626 print " -v Verbose"
627 print " -f Force installation"
628 sys.exit(1)
629
630 try:
Jack Jansenc4b217d2003-02-10 13:38:44 +0000631 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
Jack Jansen95839b82003-02-09 23:10:20 +0000632 except getopt.Error:
633 _help()
634 if not opts and not args:
635 _help()
636 mode = None
637 force = 0
638 verbose = 0
639 for o, a in opts:
640 if o == '-s':
641 if mode:
642 _help()
643 mode = 'status'
644 if o == '-l':
645 if mode:
646 _help()
647 mode = 'list'
Jack Jansenc4b217d2003-02-10 13:38:44 +0000648 if o == '-d':
Jack Jansen95839b82003-02-09 23:10:20 +0000649 if mode:
650 _help()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000651 mode = 'dump'
Jack Jansen95839b82003-02-09 23:10:20 +0000652 if o == '-i':
653 mode = 'install'
654 if o == '-f':
655 force = 1
656 if o == '-v':
657 verbose = 1
658 if not mode:
659 _help()
660 _run(mode, verbose, force, args)
661
662if __name__ == '__main__':
663 main()
664
665