blob: 2ca5369733fe07641b664ce1eb207298b640a4a5 [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'
Jack Jansen53b341f2003-02-12 15:36:25 +000036for _p in sys.path:
37 if _p[-13:] == 'site-packages':
38 DEFAULT_INSTALLDIR=_p
39 break
40else:
41 DEFAULT_INSTALLDIR=sys.prefix # Have to put things somewhere
Jack Jansen95839b82003-02-09 23:10:20 +000042DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
43
44ARCHIVE_FORMATS = [
45 (".tar.Z", "zcat \"%s\" | tar xf -"),
46 (".taz", "zcat \"%s\" | tar xf -"),
47 (".tar.gz", "zcat \"%s\" | tar xf -"),
48 (".tgz", "zcat \"%s\" | tar xf -"),
49 (".tar.bz", "bzcat \"%s\" | tar xf -"),
50]
51
Jack Jansenc4b217d2003-02-10 13:38:44 +000052class MyURLopener(urllib.FancyURLopener):
Jack Jansen8d326b82003-02-10 16:08:17 +000053 """Like FancyURLOpener, but we do want to get errors as exceptions."""
Jack Jansenc4b217d2003-02-10 13:38:44 +000054 def http_error_default(self, url, fp, errcode, errmsg, headers):
55 urllib.URLopener.http_error_default(self, url, fp, errcode, errmsg, headers)
56
Jack Jansen95839b82003-02-09 23:10:20 +000057class PimpPreferences:
Jack Jansen6a600ab2003-02-10 15:55:51 +000058 """Container for per-user preferences, such as the database to use
Jack Jansen8d326b82003-02-10 16:08:17 +000059 and where to install packages."""
Jack Jansen6a600ab2003-02-10 15:55:51 +000060
Jack Jansen95839b82003-02-09 23:10:20 +000061 def __init__(self,
62 flavorOrder=None,
63 downloadDir=None,
64 buildDir=None,
65 installDir=None,
66 pimpDatabase=None):
67 if not flavorOrder:
68 flavorOrder = DEFAULT_FLAVORORDER
69 if not downloadDir:
70 downloadDir = DEFAULT_DOWNLOADDIR
71 if not buildDir:
72 buildDir = DEFAULT_BUILDDIR
73 if not installDir:
74 installDir = DEFAULT_INSTALLDIR
75 if not pimpDatabase:
76 pimpDatabase = DEFAULT_PIMPDATABASE
77 self.flavorOrder = flavorOrder
78 self.downloadDir = downloadDir
79 self.buildDir = buildDir
80 self.installDir = installDir
81 self.pimpDatabase = pimpDatabase
82
83 def check(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +000084 """Check that the preferences make sense: directories exist and are
85 writable, the install directory is on sys.path, etc."""
86
Jack Jansen95839b82003-02-09 23:10:20 +000087 rv = ""
88 RWX_OK = os.R_OK|os.W_OK|os.X_OK
89 if not os.path.exists(self.downloadDir):
90 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
91 elif not os.access(self.downloadDir, RWX_OK):
92 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
93 if not os.path.exists(self.buildDir):
94 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
95 elif not os.access(self.buildDir, RWX_OK):
96 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
97 if not os.path.exists(self.installDir):
98 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
99 elif not os.access(self.installDir, RWX_OK):
100 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
101 else:
102 installDir = os.path.realpath(self.installDir)
103 for p in sys.path:
104 try:
105 realpath = os.path.realpath(p)
106 except:
107 pass
108 if installDir == realpath:
109 break
110 else:
111 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
112 return rv
113
114 def compareFlavors(self, left, right):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000115 """Compare two flavor strings. This is part of your preferences
116 because whether the user prefers installing from source or binary is."""
Jack Jansen95839b82003-02-09 23:10:20 +0000117 if left in self.flavorOrder:
118 if right in self.flavorOrder:
119 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
120 return -1
121 if right in self.flavorOrder:
122 return 1
123 return cmp(left, right)
124
125class PimpDatabase:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000126 """Class representing a pimp database. It can actually contain
127 information from multiple databases through inclusion, but the
128 toplevel database is considered the master, as its maintainer is
Jack Jansen8d326b82003-02-10 16:08:17 +0000129 "responsible" for the contents."""
Jack Jansen6a600ab2003-02-10 15:55:51 +0000130
Jack Jansen95839b82003-02-09 23:10:20 +0000131 def __init__(self, prefs):
132 self._packages = []
133 self.preferences = prefs
134 self._urllist = []
135 self._version = ""
136 self._maintainer = ""
137 self._description = ""
138
139 def appendURL(self, url, included=0):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000140 """Append packages from the database with the given URL.
141 Only the first database should specify included=0, so the
142 global information (maintainer, description) get stored."""
143
Jack Jansen95839b82003-02-09 23:10:20 +0000144 if url in self._urllist:
145 return
146 self._urllist.append(url)
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000147 fp = MyURLopener().open(url).fp
Jack Jansen95839b82003-02-09 23:10:20 +0000148 dict = plistlib.Plist.fromFile(fp)
149 # Test here for Pimp version, etc
150 if not included:
Jack Jansene7b33db2003-02-11 22:40:59 +0000151 self._version = dict.get('Version', '0.1')
152 if self._version != PIMP_VERSION:
153 sys.stderr.write("Warning: database version %s does not match %s\n"
154 % (self._version, PIMP_VERSION))
155 self._maintainer = dict.get('Maintainer', '')
156 self._description = dict.get('Description', '')
157 self._appendPackages(dict['Packages'])
158 others = dict.get('Include', [])
Jack Jansen95839b82003-02-09 23:10:20 +0000159 for url in others:
160 self.appendURL(url, included=1)
161
Jack Jansen6a600ab2003-02-10 15:55:51 +0000162 def _appendPackages(self, packages):
163 """Given a list of dictionaries containing package
164 descriptions create the PimpPackage objects and append them
165 to our internal storage."""
166
Jack Jansen95839b82003-02-09 23:10:20 +0000167 for p in packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000168 pkg = PimpPackage(self, dict(p))
Jack Jansen95839b82003-02-09 23:10:20 +0000169 self._packages.append(pkg)
170
171 def list(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000172 """Return a list of all PimpPackage objects in the database."""
173
Jack Jansen95839b82003-02-09 23:10:20 +0000174 return self._packages
175
176 def listnames(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000177 """Return a list of names of all packages in the database."""
178
Jack Jansen95839b82003-02-09 23:10:20 +0000179 rv = []
180 for pkg in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000181 rv.append(pkg.fullname())
Jack Jansen95839b82003-02-09 23:10:20 +0000182 return rv
183
184 def dump(self, pathOrFile):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000185 """Dump the contents of the database to an XML .plist file.
186
187 The file can be passed as either a file object or a pathname.
188 All data, including included databases, is dumped."""
189
Jack Jansen95839b82003-02-09 23:10:20 +0000190 packages = []
191 for pkg in self._packages:
192 packages.append(pkg.dump())
193 dict = {
Jack Jansene7b33db2003-02-11 22:40:59 +0000194 'Version': self._version,
195 'Maintainer': self._maintainer,
196 'Description': self._description,
197 'Packages': packages
Jack Jansen95839b82003-02-09 23:10:20 +0000198 }
199 plist = plistlib.Plist(**dict)
200 plist.write(pathOrFile)
201
202 def find(self, ident):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000203 """Find a package. The package can be specified by name
204 or as a dictionary with name, version and flavor entries.
205
206 Only name is obligatory. If there are multiple matches the
207 best one (higher version number, flavors ordered according to
208 users' preference) is returned."""
209
Jack Jansen95839b82003-02-09 23:10:20 +0000210 if type(ident) == str:
211 # Remove ( and ) for pseudo-packages
212 if ident[0] == '(' and ident[-1] == ')':
213 ident = ident[1:-1]
214 # Split into name-version-flavor
215 fields = ident.split('-')
216 if len(fields) < 1 or len(fields) > 3:
217 return None
218 name = fields[0]
219 if len(fields) > 1:
220 version = fields[1]
221 else:
222 version = None
223 if len(fields) > 2:
224 flavor = fields[2]
225 else:
226 flavor = None
227 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000228 name = ident['Name']
229 version = ident.get('Version')
230 flavor = ident.get('Flavor')
Jack Jansen95839b82003-02-09 23:10:20 +0000231 found = None
232 for p in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000233 if name == p.name() and \
234 (not version or version == p.version()) and \
235 (not flavor or flavor == p.flavor()):
Jack Jansen95839b82003-02-09 23:10:20 +0000236 if not found or found < p:
237 found = p
238 return found
239
Jack Jansene7b33db2003-02-11 22:40:59 +0000240ALLOWED_KEYS = [
241 "Name",
242 "Version",
243 "Flavor",
244 "Description",
245 "Home-page",
246 "Download-URL",
247 "Install-test",
248 "Install-command",
249 "Pre-install-command",
250 "Post-install-command",
251 "Prerequisites",
252 "MD5Sum"
253]
254
Jack Jansen95839b82003-02-09 23:10:20 +0000255class PimpPackage:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000256 """Class representing a single package."""
257
Jack Jansene7b33db2003-02-11 22:40:59 +0000258 def __init__(self, db, dict):
Jack Jansen95839b82003-02-09 23:10:20 +0000259 self._db = db
Jack Jansene7b33db2003-02-11 22:40:59 +0000260 name = dict["Name"]
261 for k in dict.keys():
262 if not k in ALLOWED_KEYS:
263 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
264 self._dict = dict
265
266 def __getitem__(self, key):
267 return self._dict[key]
Jack Jansen95839b82003-02-09 23:10:20 +0000268
Jack Jansene7b33db2003-02-11 22:40:59 +0000269 def name(self): return self._dict['Name']
270 def version(self): return self._dict['Version']
271 def flavor(self): return self._dict['Flavor']
272 def description(self): return self._dict['Description']
273 def homepage(self): return self._dict['Home-page']
274 def downloadURL(self): return self._dict['Download-URL']
275
276 def fullname(self):
277 """Return the full name "name-version-flavor" of a package.
278
279 If the package is a pseudo-package, something that cannot be
280 installed through pimp, return the name in (parentheses)."""
281
282 rv = self._dict['Name']
283 if self._dict.has_key('Version'):
284 rv = rv + '-%s' % self._dict['Version']
285 if self._dict.has_key('Flavor'):
286 rv = rv + '-%s' % self._dict['Flavor']
287 if not self._dict.get('Download-URL'):
288 # Pseudo-package, show in parentheses
289 rv = '(%s)' % rv
290 return rv
291
Jack Jansen95839b82003-02-09 23:10:20 +0000292 def dump(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000293 """Return a dict object containing the information on the package."""
Jack Jansene7b33db2003-02-11 22:40:59 +0000294 return self._dict
Jack Jansen95839b82003-02-09 23:10:20 +0000295
296 def __cmp__(self, other):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000297 """Compare two packages, where the "better" package sorts lower."""
298
Jack Jansen95839b82003-02-09 23:10:20 +0000299 if not isinstance(other, PimpPackage):
300 return cmp(id(self), id(other))
Jack Jansene7b33db2003-02-11 22:40:59 +0000301 if self.name() != other.name():
302 return cmp(self.name(), other.name())
303 if self.version() != other.version():
304 return -cmp(self.version(), other.version())
305 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Jack Jansen95839b82003-02-09 23:10:20 +0000306
307 def installed(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000308 """Test wheter the package is installed.
309
310 Returns two values: a status indicator which is one of
311 "yes", "no", "old" (an older version is installed) or "bad"
312 (something went wrong during the install test) and a human
313 readable string which may contain more details."""
314
Jack Jansen95839b82003-02-09 23:10:20 +0000315 namespace = {
316 "NotInstalled": _scriptExc_NotInstalled,
317 "OldInstalled": _scriptExc_OldInstalled,
318 "BadInstalled": _scriptExc_BadInstalled,
319 "os": os,
320 "sys": sys,
321 }
Jack Jansene7b33db2003-02-11 22:40:59 +0000322 installTest = self._dict['Install-test'].strip() + '\n'
Jack Jansen95839b82003-02-09 23:10:20 +0000323 try:
324 exec installTest in namespace
325 except ImportError, arg:
326 return "no", str(arg)
327 except _scriptExc_NotInstalled, arg:
328 return "no", str(arg)
329 except _scriptExc_OldInstalled, arg:
330 return "old", str(arg)
331 except _scriptExc_BadInstalled, arg:
332 return "bad", str(arg)
333 except:
Jack Jansen95839b82003-02-09 23:10:20 +0000334 return "bad", "Package install test got exception"
335 return "yes", ""
336
337 def prerequisites(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000338 """Return a list of prerequisites for this package.
339
340 The list contains 2-tuples, of which the first item is either
341 a PimpPackage object or None, and the second is a descriptive
342 string. The first item can be None if this package depends on
343 something that isn't pimp-installable, in which case the descriptive
344 string should tell the user what to do."""
345
Jack Jansen95839b82003-02-09 23:10:20 +0000346 rv = []
Jack Jansen53b341f2003-02-12 15:36:25 +0000347 if not self._dict.get('Download-URL'):
Jack Jansen95839b82003-02-09 23:10:20 +0000348 return [(None, "This package needs to be installed manually")]
Jack Jansene7b33db2003-02-11 22:40:59 +0000349 if not self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000350 return []
Jack Jansene7b33db2003-02-11 22:40:59 +0000351 for item in self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000352 if type(item) == str:
353 pkg = None
354 descr = str(item)
355 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000356 name = item['Name']
357 if item.has_key('Version'):
358 name = name + '-' + item['Version']
359 if item.has_key('Flavor'):
360 name = name + '-' + item['Flavor']
361 pkg = self._db.find(name)
Jack Jansen95839b82003-02-09 23:10:20 +0000362 if not pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000363 descr = "Requires unknown %s"%name
Jack Jansen95839b82003-02-09 23:10:20 +0000364 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000365 descr = pkg.description()
Jack Jansen95839b82003-02-09 23:10:20 +0000366 rv.append((pkg, descr))
367 return rv
368
369 def _cmd(self, output, dir, *cmditems):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000370 """Internal routine to run a shell command in a given directory."""
371
Jack Jansen95839b82003-02-09 23:10:20 +0000372 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
373 if output:
374 output.write("+ %s\n" % cmd)
375 if NO_EXECUTE:
376 return 0
Jack Jansen53b341f2003-02-12 15:36:25 +0000377 dummy, fp = os.popen4(cmd, "r")
378 dummy.close()
Jack Jansen95839b82003-02-09 23:10:20 +0000379 while 1:
380 line = fp.readline()
381 if not line:
382 break
383 if output:
384 output.write(line)
385 rv = fp.close()
386 return rv
387
Jack Jansen6a600ab2003-02-10 15:55:51 +0000388 def downloadSinglePackage(self, output=None):
389 """Download a single package, if needed.
390
391 An MD5 signature is used to determine whether download is needed,
392 and to test that we actually downloaded what we expected.
393 If output is given it is a file-like object that will receive a log
394 of what happens.
395
396 If anything unforeseen happened the method returns an error message
397 string.
398 """
399
Jack Jansene7b33db2003-02-11 22:40:59 +0000400 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
Jack Jansen95839b82003-02-09 23:10:20 +0000401 path = urllib.url2pathname(path)
402 filename = os.path.split(path)[1]
Jack Jansenc4b217d2003-02-10 13:38:44 +0000403 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
404 if not self._archiveOK():
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000405 if scheme == 'manual':
406 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansenc4b217d2003-02-10 13:38:44 +0000407 if self._cmd(output, self._db.preferences.downloadDir,
408 "curl",
409 "--output", self.archiveFilename,
Jack Jansene7b33db2003-02-11 22:40:59 +0000410 self._dict['Download-URL']):
Jack Jansenc4b217d2003-02-10 13:38:44 +0000411 return "download command failed"
Jack Jansen95839b82003-02-09 23:10:20 +0000412 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
413 return "archive not found after download"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000414 if not self._archiveOK():
415 return "archive does not have correct MD5 checksum"
416
417 def _archiveOK(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000418 """Test an archive. It should exist and the MD5 checksum should be correct."""
419
Jack Jansenc4b217d2003-02-10 13:38:44 +0000420 if not os.path.exists(self.archiveFilename):
421 return 0
Jack Jansene7b33db2003-02-11 22:40:59 +0000422 if not self._dict['MD5Sum']:
423 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
Jack Jansenc4b217d2003-02-10 13:38:44 +0000424 return 1
425 data = open(self.archiveFilename, 'rb').read()
426 checksum = md5.new(data).hexdigest()
Jack Jansene7b33db2003-02-11 22:40:59 +0000427 return checksum == self._dict['MD5Sum']
Jack Jansen95839b82003-02-09 23:10:20 +0000428
Jack Jansen6a600ab2003-02-10 15:55:51 +0000429 def unpackSinglePackage(self, output=None):
430 """Unpack a downloaded package archive."""
431
Jack Jansen95839b82003-02-09 23:10:20 +0000432 filename = os.path.split(self.archiveFilename)[1]
433 for ext, cmd in ARCHIVE_FORMATS:
434 if filename[-len(ext):] == ext:
435 break
436 else:
437 return "unknown extension for archive file: %s" % filename
438 basename = filename[:-len(ext)]
439 cmd = cmd % self.archiveFilename
440 self._buildDirname = os.path.join(self._db.preferences.buildDir, basename)
441 if self._cmd(output, self._db.preferences.buildDir, cmd):
442 return "unpack command failed"
443 setupname = os.path.join(self._buildDirname, "setup.py")
444 if not os.path.exists(setupname) and not NO_EXECUTE:
445 return "no setup.py found after unpack of archive"
446
Jack Jansen6a600ab2003-02-10 15:55:51 +0000447 def installSinglePackage(self, output=None):
448 """Download, unpack and install a single package.
449
450 If output is given it should be a file-like object and it
451 will receive a log of what happened."""
452
Jack Jansene7b33db2003-02-11 22:40:59 +0000453 if not self._dict['Download-URL']:
Jack Jansen95839b82003-02-09 23:10:20 +0000454 return "%s: This package needs to be installed manually" % _fmtpackagename(self)
455 msg = self.downloadSinglePackage(output)
456 if msg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000457 return "download %s: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000458
Jack Jansen95839b82003-02-09 23:10:20 +0000459 msg = self.unpackSinglePackage(output)
460 if msg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000461 return "unpack %s: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000462
Jack Jansene7b33db2003-02-11 22:40:59 +0000463 if self._dict.has_key('Pre-install-command'):
464 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000465 return "pre-install %s: running \"%s\" failed" % \
Jack Jansene7b33db2003-02-11 22:40:59 +0000466 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen53b341f2003-02-12 15:36:25 +0000467
468 old_contents = os.listdir(self._db.preferences.installDir)
Jack Jansene7b33db2003-02-11 22:40:59 +0000469 installcmd = self._dict.get('Install-command')
470 if not installcmd:
471 installcmd = '"%s" setup.py install' % sys.executable
472 if self._cmd(output, self._buildDirname, installcmd):
473 return "install %s: running \"%s\" failed" % self.fullname()
Jack Jansen53b341f2003-02-12 15:36:25 +0000474
475 new_contents = os.listdir(self._db.preferences.installDir)
476 self._interpretPthFiles(old_contents, new_contents)
477
Jack Jansene7b33db2003-02-11 22:40:59 +0000478 if self._dict.has_key('Post-install-command'):
479 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000480 return "post-install %s: running \"%s\" failed" % \
Jack Jansene7b33db2003-02-11 22:40:59 +0000481 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen95839b82003-02-09 23:10:20 +0000482 return None
Jack Jansen53b341f2003-02-12 15:36:25 +0000483
484 def _interpretPthFiles(self, old_contents, new_contents):
485 """Evaluate any new .pth files that have appeared after installing"""
486 for fn in new_contents:
487 if fn in old_contents:
488 continue
489 if fn[-4:] != '.pth':
490 continue
491 fullname = os.path.join(self._db.preferences.installDir, fn)
492 f = open(fullname)
493 for line in f.readlines():
494 if not line:
495 continue
496 if line[0] == '#':
497 continue
498 if line[:6] == 'import':
499 exec line
500 continue
501 if line[-1] == '\n':
502 line = line[:-1]
503 if not os.path.isabs(line):
504 line = os.path.join(self._db.preferences.installDir, line)
505 line = os.path.realpath(line)
506 if not line in sys.path:
507 sys.path.append(line)
508
Jack Jansen95839b82003-02-09 23:10:20 +0000509
510class PimpInstaller:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000511 """Installer engine: computes dependencies and installs
512 packages in the right order."""
513
Jack Jansen95839b82003-02-09 23:10:20 +0000514 def __init__(self, db):
515 self._todo = []
516 self._db = db
517 self._curtodo = []
518 self._curmessages = []
519
520 def __contains__(self, package):
521 return package in self._todo
522
523 def _addPackages(self, packages):
524 for package in packages:
525 if not package in self._todo:
526 self._todo.insert(0, package)
527
528 def _prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000529 """Internal routine, recursive engine for prepareInstall.
530
531 Test whether the package is installed and (if not installed
532 or if force==1) prepend it to the temporary todo list and
533 call ourselves recursively on all prerequisites."""
534
Jack Jansen95839b82003-02-09 23:10:20 +0000535 if not force:
536 status, message = package.installed()
537 if status == "yes":
538 return
539 if package in self._todo or package in self._curtodo:
540 return
541 self._curtodo.insert(0, package)
542 if not recursive:
543 return
544 prereqs = package.prerequisites()
545 for pkg, descr in prereqs:
546 if pkg:
547 self._prepareInstall(pkg, force, recursive)
548 else:
549 self._curmessages.append("Requires: %s" % descr)
550
551 def prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000552 """Prepare installation of a package.
553
554 If the package is already installed and force is false nothing
555 is done. If recursive is true prerequisites are installed first.
556
557 Returns a list of packages (to be passed to install) and a list
558 of messages of any problems encountered.
559 """
560
Jack Jansen95839b82003-02-09 23:10:20 +0000561 self._curtodo = []
562 self._curmessages = []
563 self._prepareInstall(package, force, recursive)
564 rv = self._curtodo, self._curmessages
565 self._curtodo = []
566 self._curmessages = []
567 return rv
568
569 def install(self, packages, output):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000570 """Install a list of packages."""
571
Jack Jansen95839b82003-02-09 23:10:20 +0000572 self._addPackages(packages)
573 status = []
574 for pkg in self._todo:
575 msg = pkg.installSinglePackage(output)
576 if msg:
577 status.append(msg)
578 return status
579
580
Jack Jansen95839b82003-02-09 23:10:20 +0000581
582def _run(mode, verbose, force, args):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000583 """Engine for the main program"""
584
Jack Jansen95839b82003-02-09 23:10:20 +0000585 prefs = PimpPreferences()
586 prefs.check()
587 db = PimpDatabase(prefs)
588 db.appendURL(prefs.pimpDatabase)
589
Jack Jansenc4b217d2003-02-10 13:38:44 +0000590 if mode == 'dump':
591 db.dump(sys.stdout)
592 elif mode =='list':
Jack Jansen95839b82003-02-09 23:10:20 +0000593 if not args:
594 args = db.listnames()
595 print "%-20.20s\t%s" % ("Package", "Description")
596 print
597 for pkgname in args:
598 pkg = db.find(pkgname)
599 if pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000600 description = pkg.description()
601 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000602 else:
603 description = 'Error: no such package'
604 print "%-20.20s\t%s" % (pkgname, description)
605 if verbose:
Jack Jansene7b33db2003-02-11 22:40:59 +0000606 print "\tHome page:\t", pkg.homepage()
607 print "\tDownload URL:\t", pkg.downloadURL()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000608 elif mode =='status':
Jack Jansen95839b82003-02-09 23:10:20 +0000609 if not args:
610 args = db.listnames()
611 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
612 print
613 for pkgname in args:
614 pkg = db.find(pkgname)
615 if pkg:
616 status, msg = pkg.installed()
Jack Jansene7b33db2003-02-11 22:40:59 +0000617 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000618 else:
619 status = 'error'
620 msg = 'No such package'
621 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
622 if verbose and status == "no":
623 prereq = pkg.prerequisites()
624 for pkg, msg in prereq:
625 if not pkg:
626 pkg = ''
627 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000628 pkg = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000629 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
630 elif mode == 'install':
631 if not args:
632 print 'Please specify packages to install'
633 sys.exit(1)
634 inst = PimpInstaller(db)
635 for pkgname in args:
636 pkg = db.find(pkgname)
637 if not pkg:
638 print '%s: No such package' % pkgname
639 continue
640 list, messages = inst.prepareInstall(pkg, force)
641 if messages and not force:
642 print "%s: Not installed:" % pkgname
643 for m in messages:
644 print "\t", m
645 else:
646 if verbose:
647 output = sys.stdout
648 else:
649 output = None
650 messages = inst.install(list, output)
651 if messages:
652 print "%s: Not installed:" % pkgname
653 for m in messages:
654 print "\t", m
655
656def main():
Jack Jansen6a600ab2003-02-10 15:55:51 +0000657 """Minimal commandline tool to drive pimp."""
658
Jack Jansen95839b82003-02-09 23:10:20 +0000659 import getopt
660 def _help():
661 print "Usage: pimp [-v] -s [package ...] List installed status"
662 print " pimp [-v] -l [package ...] Show package information"
663 print " pimp [-vf] -i package ... Install packages"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000664 print " pimp -d Dump database to stdout"
Jack Jansen95839b82003-02-09 23:10:20 +0000665 print "Options:"
666 print " -v Verbose"
667 print " -f Force installation"
668 sys.exit(1)
669
670 try:
Jack Jansenc4b217d2003-02-10 13:38:44 +0000671 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
Jack Jansen95839b82003-02-09 23:10:20 +0000672 except getopt.Error:
673 _help()
674 if not opts and not args:
675 _help()
676 mode = None
677 force = 0
678 verbose = 0
679 for o, a in opts:
680 if o == '-s':
681 if mode:
682 _help()
683 mode = 'status'
684 if o == '-l':
685 if mode:
686 _help()
687 mode = 'list'
Jack Jansenc4b217d2003-02-10 13:38:44 +0000688 if o == '-d':
Jack Jansen95839b82003-02-09 23:10:20 +0000689 if mode:
690 _help()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000691 mode = 'dump'
Jack Jansen95839b82003-02-09 23:10:20 +0000692 if o == '-i':
693 mode = 'install'
694 if o == '-f':
695 force = 1
696 if o == '-v':
697 verbose = 1
698 if not mode:
699 _help()
700 _run(mode, verbose, force, args)
701
702if __name__ == '__main__':
703 main()
704
705