blob: c5cf45eb3565d9e74d8f60546c267e3ace3a8996 [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 Jansene71b9f82003-02-12 16:37:00 +000021import distutils.sysconfig
Jack Jansenc4b217d2003-02-10 13:38:44 +000022import md5
Jack Jansen95839b82003-02-09 23:10:20 +000023
Jack Jansen6a600ab2003-02-10 15:55:51 +000024__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main"]
25
Jack Jansen95839b82003-02-09 23:10:20 +000026_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
27_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
28_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
29
30NO_EXECUTE=0
31
Jack Jansene7b33db2003-02-11 22:40:59 +000032PIMP_VERSION="0.1"
33
Jack Jansen0dacac42003-02-14 14:11:59 +000034# Flavors:
35# source: setup-based package
36# binary: tar (or other) archive created with setup.py bdist.
Jack Jansen95839b82003-02-09 23:10:20 +000037DEFAULT_FLAVORORDER=['source', 'binary']
38DEFAULT_DOWNLOADDIR='/tmp'
39DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000040DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansen95839b82003-02-09 23:10:20 +000041DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
42
43ARCHIVE_FORMATS = [
Jack Jansen0dacac42003-02-14 14:11:59 +000044 (".tar.Z", "zcat \"%s\" | tar -xf -"),
45 (".taz", "zcat \"%s\" | tar -xf -"),
46 (".tar.gz", "zcat \"%s\" | tar -xf -"),
47 (".tgz", "zcat \"%s\" | tar -xf -"),
48 (".tar.bz", "bzcat \"%s\" | tar -xf -"),
Jack Jansena460f442003-02-17 12:21:05 +000049 (".zip", "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +000050]
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
Jack Jansen0dacac42003-02-14 14:11:59 +0000139 def close(self):
140 """Clean up"""
141 self._packages = []
142 self.preferences = None
143
Jack Jansen95839b82003-02-09 23:10:20 +0000144 def appendURL(self, url, included=0):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000145 """Append packages from the database with the given URL.
146 Only the first database should specify included=0, so the
147 global information (maintainer, description) get stored."""
148
Jack Jansen95839b82003-02-09 23:10:20 +0000149 if url in self._urllist:
150 return
151 self._urllist.append(url)
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000152 fp = MyURLopener().open(url).fp
Jack Jansen95839b82003-02-09 23:10:20 +0000153 dict = plistlib.Plist.fromFile(fp)
154 # Test here for Pimp version, etc
155 if not included:
Jack Jansene7b33db2003-02-11 22:40:59 +0000156 self._version = dict.get('Version', '0.1')
157 if self._version != PIMP_VERSION:
158 sys.stderr.write("Warning: database version %s does not match %s\n"
159 % (self._version, PIMP_VERSION))
160 self._maintainer = dict.get('Maintainer', '')
161 self._description = dict.get('Description', '')
162 self._appendPackages(dict['Packages'])
163 others = dict.get('Include', [])
Jack Jansen95839b82003-02-09 23:10:20 +0000164 for url in others:
165 self.appendURL(url, included=1)
166
Jack Jansen6a600ab2003-02-10 15:55:51 +0000167 def _appendPackages(self, packages):
168 """Given a list of dictionaries containing package
169 descriptions create the PimpPackage objects and append them
170 to our internal storage."""
171
Jack Jansen95839b82003-02-09 23:10:20 +0000172 for p in packages:
Jack Jansen0dacac42003-02-14 14:11:59 +0000173 p = dict(p)
174 flavor = p.get('Flavor')
175 if flavor == 'source':
176 pkg = PimpPackage_source(self, p)
177 elif flavor == 'binary':
178 pkg = PimpPackage_binary(self, p)
179 else:
180 pkg = PimpPackage(self, dict(p))
Jack Jansen95839b82003-02-09 23:10:20 +0000181 self._packages.append(pkg)
182
183 def list(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000184 """Return a list of all PimpPackage objects in the database."""
185
Jack Jansen95839b82003-02-09 23:10:20 +0000186 return self._packages
187
188 def listnames(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000189 """Return a list of names of all packages in the database."""
190
Jack Jansen95839b82003-02-09 23:10:20 +0000191 rv = []
192 for pkg in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000193 rv.append(pkg.fullname())
Jack Jansen0dacac42003-02-14 14:11:59 +0000194 rv.sort()
Jack Jansen95839b82003-02-09 23:10:20 +0000195 return rv
196
197 def dump(self, pathOrFile):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000198 """Dump the contents of the database to an XML .plist file.
199
200 The file can be passed as either a file object or a pathname.
201 All data, including included databases, is dumped."""
202
Jack Jansen95839b82003-02-09 23:10:20 +0000203 packages = []
204 for pkg in self._packages:
205 packages.append(pkg.dump())
206 dict = {
Jack Jansene7b33db2003-02-11 22:40:59 +0000207 'Version': self._version,
208 'Maintainer': self._maintainer,
209 'Description': self._description,
210 'Packages': packages
Jack Jansen95839b82003-02-09 23:10:20 +0000211 }
212 plist = plistlib.Plist(**dict)
213 plist.write(pathOrFile)
214
215 def find(self, ident):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000216 """Find a package. The package can be specified by name
217 or as a dictionary with name, version and flavor entries.
218
219 Only name is obligatory. If there are multiple matches the
220 best one (higher version number, flavors ordered according to
221 users' preference) is returned."""
222
Jack Jansen95839b82003-02-09 23:10:20 +0000223 if type(ident) == str:
224 # Remove ( and ) for pseudo-packages
225 if ident[0] == '(' and ident[-1] == ')':
226 ident = ident[1:-1]
227 # Split into name-version-flavor
228 fields = ident.split('-')
229 if len(fields) < 1 or len(fields) > 3:
230 return None
231 name = fields[0]
232 if len(fields) > 1:
233 version = fields[1]
234 else:
235 version = None
236 if len(fields) > 2:
237 flavor = fields[2]
238 else:
239 flavor = None
240 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000241 name = ident['Name']
242 version = ident.get('Version')
243 flavor = ident.get('Flavor')
Jack Jansen95839b82003-02-09 23:10:20 +0000244 found = None
245 for p in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000246 if name == p.name() and \
247 (not version or version == p.version()) and \
248 (not flavor or flavor == p.flavor()):
Jack Jansen95839b82003-02-09 23:10:20 +0000249 if not found or found < p:
250 found = p
251 return found
252
Jack Jansene7b33db2003-02-11 22:40:59 +0000253ALLOWED_KEYS = [
254 "Name",
255 "Version",
256 "Flavor",
257 "Description",
258 "Home-page",
259 "Download-URL",
260 "Install-test",
261 "Install-command",
262 "Pre-install-command",
263 "Post-install-command",
264 "Prerequisites",
265 "MD5Sum"
266]
267
Jack Jansen95839b82003-02-09 23:10:20 +0000268class PimpPackage:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000269 """Class representing a single package."""
270
Jack Jansene7b33db2003-02-11 22:40:59 +0000271 def __init__(self, db, dict):
Jack Jansen95839b82003-02-09 23:10:20 +0000272 self._db = db
Jack Jansene7b33db2003-02-11 22:40:59 +0000273 name = dict["Name"]
274 for k in dict.keys():
275 if not k in ALLOWED_KEYS:
276 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
277 self._dict = dict
278
279 def __getitem__(self, key):
280 return self._dict[key]
Jack Jansen95839b82003-02-09 23:10:20 +0000281
Jack Jansene7b33db2003-02-11 22:40:59 +0000282 def name(self): return self._dict['Name']
283 def version(self): return self._dict['Version']
284 def flavor(self): return self._dict['Flavor']
285 def description(self): return self._dict['Description']
Jack Jansen0dacac42003-02-14 14:11:59 +0000286 def homepage(self): return self._dict.get('Home-page')
Jack Jansene7b33db2003-02-11 22:40:59 +0000287 def downloadURL(self): return self._dict['Download-URL']
288
289 def fullname(self):
290 """Return the full name "name-version-flavor" of a package.
291
292 If the package is a pseudo-package, something that cannot be
293 installed through pimp, return the name in (parentheses)."""
294
295 rv = self._dict['Name']
296 if self._dict.has_key('Version'):
297 rv = rv + '-%s' % self._dict['Version']
298 if self._dict.has_key('Flavor'):
299 rv = rv + '-%s' % self._dict['Flavor']
300 if not self._dict.get('Download-URL'):
301 # Pseudo-package, show in parentheses
302 rv = '(%s)' % rv
303 return rv
304
Jack Jansen95839b82003-02-09 23:10:20 +0000305 def dump(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000306 """Return a dict object containing the information on the package."""
Jack Jansene7b33db2003-02-11 22:40:59 +0000307 return self._dict
Jack Jansen95839b82003-02-09 23:10:20 +0000308
309 def __cmp__(self, other):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000310 """Compare two packages, where the "better" package sorts lower."""
311
Jack Jansen95839b82003-02-09 23:10:20 +0000312 if not isinstance(other, PimpPackage):
313 return cmp(id(self), id(other))
Jack Jansene7b33db2003-02-11 22:40:59 +0000314 if self.name() != other.name():
315 return cmp(self.name(), other.name())
316 if self.version() != other.version():
317 return -cmp(self.version(), other.version())
318 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Jack Jansen95839b82003-02-09 23:10:20 +0000319
320 def installed(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000321 """Test wheter the package is installed.
322
323 Returns two values: a status indicator which is one of
324 "yes", "no", "old" (an older version is installed) or "bad"
325 (something went wrong during the install test) and a human
326 readable string which may contain more details."""
327
Jack Jansen95839b82003-02-09 23:10:20 +0000328 namespace = {
329 "NotInstalled": _scriptExc_NotInstalled,
330 "OldInstalled": _scriptExc_OldInstalled,
331 "BadInstalled": _scriptExc_BadInstalled,
332 "os": os,
333 "sys": sys,
334 }
Jack Jansene7b33db2003-02-11 22:40:59 +0000335 installTest = self._dict['Install-test'].strip() + '\n'
Jack Jansen95839b82003-02-09 23:10:20 +0000336 try:
337 exec installTest in namespace
338 except ImportError, arg:
339 return "no", str(arg)
340 except _scriptExc_NotInstalled, arg:
341 return "no", str(arg)
342 except _scriptExc_OldInstalled, arg:
343 return "old", str(arg)
344 except _scriptExc_BadInstalled, arg:
345 return "bad", str(arg)
346 except:
Jack Jansena460f442003-02-17 12:21:05 +0000347 sys.stderr.write("-------------------------------------\n")
348 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
349 sys.stderr.write("---- source:\n")
350 sys.stderr.write(installTest)
351 sys.stderr.write("---- exception:\n")
352 import traceback
353 traceback.print_exc(file=sys.stderr)
354 if self._db._maintainer:
355 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
356 sys.stderr.write("-------------------------------------\n")
Jack Jansen95839b82003-02-09 23:10:20 +0000357 return "bad", "Package install test got exception"
358 return "yes", ""
359
360 def prerequisites(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000361 """Return a list of prerequisites for this package.
362
363 The list contains 2-tuples, of which the first item is either
364 a PimpPackage object or None, and the second is a descriptive
365 string. The first item can be None if this package depends on
366 something that isn't pimp-installable, in which case the descriptive
367 string should tell the user what to do."""
368
Jack Jansen95839b82003-02-09 23:10:20 +0000369 rv = []
Jack Jansen53b341f2003-02-12 15:36:25 +0000370 if not self._dict.get('Download-URL'):
Jack Jansena2125fe2003-02-16 23:03:04 +0000371 return [(None,
372 "%s: This package needs to be installed manually (no Download-URL field)" %
373 self.fullname())]
Jack Jansen0dacac42003-02-14 14:11:59 +0000374 if not self._dict.get('Prerequisites'):
Jack Jansen95839b82003-02-09 23:10:20 +0000375 return []
Jack Jansene7b33db2003-02-11 22:40:59 +0000376 for item in self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000377 if type(item) == str:
378 pkg = None
379 descr = str(item)
380 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000381 name = item['Name']
382 if item.has_key('Version'):
383 name = name + '-' + item['Version']
384 if item.has_key('Flavor'):
385 name = name + '-' + item['Flavor']
386 pkg = self._db.find(name)
Jack Jansen95839b82003-02-09 23:10:20 +0000387 if not pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000388 descr = "Requires unknown %s"%name
Jack Jansen95839b82003-02-09 23:10:20 +0000389 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000390 descr = pkg.description()
Jack Jansen95839b82003-02-09 23:10:20 +0000391 rv.append((pkg, descr))
392 return rv
393
394 def _cmd(self, output, dir, *cmditems):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000395 """Internal routine to run a shell command in a given directory."""
396
Jack Jansen95839b82003-02-09 23:10:20 +0000397 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
398 if output:
399 output.write("+ %s\n" % cmd)
400 if NO_EXECUTE:
401 return 0
Jack Jansen53b341f2003-02-12 15:36:25 +0000402 dummy, fp = os.popen4(cmd, "r")
403 dummy.close()
Jack Jansen95839b82003-02-09 23:10:20 +0000404 while 1:
405 line = fp.readline()
406 if not line:
407 break
408 if output:
409 output.write(line)
410 rv = fp.close()
411 return rv
412
Jack Jansen0dacac42003-02-14 14:11:59 +0000413 def downloadPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000414 """Download a single package, if needed.
415
416 An MD5 signature is used to determine whether download is needed,
417 and to test that we actually downloaded what we expected.
418 If output is given it is a file-like object that will receive a log
419 of what happens.
420
421 If anything unforeseen happened the method returns an error message
422 string.
423 """
424
Jack Jansene7b33db2003-02-11 22:40:59 +0000425 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
Jack Jansen95839b82003-02-09 23:10:20 +0000426 path = urllib.url2pathname(path)
427 filename = os.path.split(path)[1]
Jack Jansenc4b217d2003-02-10 13:38:44 +0000428 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
429 if not self._archiveOK():
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000430 if scheme == 'manual':
431 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansenc4b217d2003-02-10 13:38:44 +0000432 if self._cmd(output, self._db.preferences.downloadDir,
433 "curl",
434 "--output", self.archiveFilename,
Jack Jansene7b33db2003-02-11 22:40:59 +0000435 self._dict['Download-URL']):
Jack Jansenc4b217d2003-02-10 13:38:44 +0000436 return "download command failed"
Jack Jansen95839b82003-02-09 23:10:20 +0000437 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
438 return "archive not found after download"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000439 if not self._archiveOK():
440 return "archive does not have correct MD5 checksum"
441
442 def _archiveOK(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000443 """Test an archive. It should exist and the MD5 checksum should be correct."""
444
Jack Jansenc4b217d2003-02-10 13:38:44 +0000445 if not os.path.exists(self.archiveFilename):
446 return 0
Jack Jansene71b9f82003-02-12 16:37:00 +0000447 if not self._dict.get('MD5Sum'):
Jack Jansene7b33db2003-02-11 22:40:59 +0000448 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
Jack Jansenc4b217d2003-02-10 13:38:44 +0000449 return 1
450 data = open(self.archiveFilename, 'rb').read()
451 checksum = md5.new(data).hexdigest()
Jack Jansene7b33db2003-02-11 22:40:59 +0000452 return checksum == self._dict['MD5Sum']
Jack Jansen95839b82003-02-09 23:10:20 +0000453
Jack Jansen0dacac42003-02-14 14:11:59 +0000454 def unpackPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000455 """Unpack a downloaded package archive."""
456
Jack Jansen95839b82003-02-09 23:10:20 +0000457 filename = os.path.split(self.archiveFilename)[1]
458 for ext, cmd in ARCHIVE_FORMATS:
459 if filename[-len(ext):] == ext:
460 break
461 else:
462 return "unknown extension for archive file: %s" % filename
Jack Jansena2125fe2003-02-16 23:03:04 +0000463 self.basename = filename[:-len(ext)]
Jack Jansen95839b82003-02-09 23:10:20 +0000464 cmd = cmd % self.archiveFilename
Jack Jansen95839b82003-02-09 23:10:20 +0000465 if self._cmd(output, self._db.preferences.buildDir, cmd):
466 return "unpack command failed"
Jack Jansen0dacac42003-02-14 14:11:59 +0000467
468 def installPackageOnly(self, output=None):
469 """Default install method, to be overridden by subclasses"""
Jack Jansena2125fe2003-02-16 23:03:04 +0000470 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
471 % (self.fullname(), self._dict.get(flavor, ""))
Jack Jansen95839b82003-02-09 23:10:20 +0000472
Jack Jansen6a600ab2003-02-10 15:55:51 +0000473 def installSinglePackage(self, output=None):
474 """Download, unpack and install a single package.
475
476 If output is given it should be a file-like object and it
477 will receive a log of what happened."""
478
Jack Jansene7b33db2003-02-11 22:40:59 +0000479 if not self._dict['Download-URL']:
Jack Jansena2125fe2003-02-16 23:03:04 +0000480 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
Jack Jansen0dacac42003-02-14 14:11:59 +0000481 msg = self.downloadPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000482 if msg:
Jack Jansena2125fe2003-02-16 23:03:04 +0000483 return "%s: download: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000484
Jack Jansen0dacac42003-02-14 14:11:59 +0000485 msg = self.unpackPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000486 if msg:
Jack Jansena2125fe2003-02-16 23:03:04 +0000487 return "%s: unpack: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000488
Jack Jansen0dacac42003-02-14 14:11:59 +0000489 return self.installPackageOnly(output)
Jack Jansen53b341f2003-02-12 15:36:25 +0000490
Jack Jansen0dacac42003-02-14 14:11:59 +0000491 def beforeInstall(self):
492 """Bookkeeping before installation: remember what we have in site-packages"""
493 self._old_contents = os.listdir(self._db.preferences.installDir)
494
495 def afterInstall(self):
496 """Bookkeeping after installation: interpret any new .pth files that have
497 appeared"""
498
Jack Jansen53b341f2003-02-12 15:36:25 +0000499 new_contents = os.listdir(self._db.preferences.installDir)
Jack Jansen53b341f2003-02-12 15:36:25 +0000500 for fn in new_contents:
Jack Jansen0dacac42003-02-14 14:11:59 +0000501 if fn in self._old_contents:
Jack Jansen53b341f2003-02-12 15:36:25 +0000502 continue
503 if fn[-4:] != '.pth':
504 continue
505 fullname = os.path.join(self._db.preferences.installDir, fn)
506 f = open(fullname)
507 for line in f.readlines():
508 if not line:
509 continue
510 if line[0] == '#':
511 continue
512 if line[:6] == 'import':
513 exec line
514 continue
515 if line[-1] == '\n':
516 line = line[:-1]
517 if not os.path.isabs(line):
518 line = os.path.join(self._db.preferences.installDir, line)
519 line = os.path.realpath(line)
520 if not line in sys.path:
Jack Jansen0dacac42003-02-14 14:11:59 +0000521 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000522
Jack Jansen0dacac42003-02-14 14:11:59 +0000523class PimpPackage_binary(PimpPackage):
524
525 def unpackPackageOnly(self, output=None):
526 """We don't unpack binary packages until installing"""
527 pass
528
529 def installPackageOnly(self, output=None):
530 """Install a single source package.
531
532 If output is given it should be a file-like object and it
533 will receive a log of what happened."""
Jack Jansena2125fe2003-02-16 23:03:04 +0000534 print 'PimpPackage_binary installPackageOnly'
Jack Jansen0dacac42003-02-14 14:11:59 +0000535
536 msgs = []
537 if self._dict.has_key('Pre-install-command'):
538 msg.append("%s: Pre-install-command ignored" % self.fullname())
539 if self._dict.has_key('Install-command'):
540 msgs.append("%s: Install-command ignored" % self.fullname())
541 if self._dict.has_key('Post-install-command'):
542 msgs.append("%s: Post-install-command ignored" % self.fullname())
543
544 self.beforeInstall()
545
546 # Install by unpacking
547 filename = os.path.split(self.archiveFilename)[1]
548 for ext, cmd in ARCHIVE_FORMATS:
549 if filename[-len(ext):] == ext:
550 break
551 else:
552 return "unknown extension for archive file: %s" % filename
553
Jack Jansena460f442003-02-17 12:21:05 +0000554 # Extract the files in the root folder.
Jack Jansen0dacac42003-02-14 14:11:59 +0000555 cmd = cmd % self.archiveFilename
Jack Jansena460f442003-02-17 12:21:05 +0000556 if self._cmd(output, "/", cmd):
Jack Jansen0dacac42003-02-14 14:11:59 +0000557 return "unpack command failed"
558
559 self.afterInstall()
560
561 if self._dict.has_key('Post-install-command'):
562 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
563 return "post-install %s: running \"%s\" failed" % \
564 (self.fullname(), self._dict['Post-install-command'])
565 return None
566
567
568class PimpPackage_source(PimpPackage):
569
570 def unpackPackageOnly(self, output=None):
571 """Unpack a source package and check that setup.py exists"""
572 PimpPackage.unpackPackageOnly(self, output)
573 # Test that a setup script has been create
Jack Jansena2125fe2003-02-16 23:03:04 +0000574 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
Jack Jansen0dacac42003-02-14 14:11:59 +0000575 setupname = os.path.join(self._buildDirname, "setup.py")
576 if not os.path.exists(setupname) and not NO_EXECUTE:
577 return "no setup.py found after unpack of archive"
578
579 def installPackageOnly(self, output=None):
580 """Install a single source package.
581
582 If output is given it should be a file-like object and it
583 will receive a log of what happened."""
584
585 if self._dict.has_key('Pre-install-command'):
586 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
587 return "pre-install %s: running \"%s\" failed" % \
588 (self.fullname(), self._dict['Pre-install-command'])
589
590 self.beforeInstall()
591 installcmd = self._dict.get('Install-command')
592 if not installcmd:
593 installcmd = '"%s" setup.py install' % sys.executable
594 if self._cmd(output, self._buildDirname, installcmd):
595 return "install %s: running \"%s\" failed" % self.fullname()
596
597 self.afterInstall()
598
599 if self._dict.has_key('Post-install-command'):
600 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
601 return "post-install %s: running \"%s\" failed" % \
602 (self.fullname(), self._dict['Post-install-command'])
603 return None
604
605
Jack Jansen95839b82003-02-09 23:10:20 +0000606class PimpInstaller:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000607 """Installer engine: computes dependencies and installs
608 packages in the right order."""
609
Jack Jansen95839b82003-02-09 23:10:20 +0000610 def __init__(self, db):
611 self._todo = []
612 self._db = db
613 self._curtodo = []
614 self._curmessages = []
615
616 def __contains__(self, package):
617 return package in self._todo
618
619 def _addPackages(self, packages):
620 for package in packages:
621 if not package in self._todo:
622 self._todo.insert(0, package)
623
624 def _prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000625 """Internal routine, recursive engine for prepareInstall.
626
627 Test whether the package is installed and (if not installed
628 or if force==1) prepend it to the temporary todo list and
629 call ourselves recursively on all prerequisites."""
630
Jack Jansen95839b82003-02-09 23:10:20 +0000631 if not force:
632 status, message = package.installed()
633 if status == "yes":
634 return
635 if package in self._todo or package in self._curtodo:
636 return
637 self._curtodo.insert(0, package)
638 if not recursive:
639 return
640 prereqs = package.prerequisites()
641 for pkg, descr in prereqs:
642 if pkg:
643 self._prepareInstall(pkg, force, recursive)
644 else:
645 self._curmessages.append("Requires: %s" % descr)
646
647 def prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000648 """Prepare installation of a package.
649
650 If the package is already installed and force is false nothing
651 is done. If recursive is true prerequisites are installed first.
652
653 Returns a list of packages (to be passed to install) and a list
654 of messages of any problems encountered.
655 """
656
Jack Jansen95839b82003-02-09 23:10:20 +0000657 self._curtodo = []
658 self._curmessages = []
659 self._prepareInstall(package, force, recursive)
660 rv = self._curtodo, self._curmessages
661 self._curtodo = []
662 self._curmessages = []
663 return rv
664
665 def install(self, packages, output):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000666 """Install a list of packages."""
667
Jack Jansen95839b82003-02-09 23:10:20 +0000668 self._addPackages(packages)
669 status = []
670 for pkg in self._todo:
671 msg = pkg.installSinglePackage(output)
672 if msg:
673 status.append(msg)
674 return status
675
676
Jack Jansen95839b82003-02-09 23:10:20 +0000677
678def _run(mode, verbose, force, args):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000679 """Engine for the main program"""
680
Jack Jansen95839b82003-02-09 23:10:20 +0000681 prefs = PimpPreferences()
682 prefs.check()
683 db = PimpDatabase(prefs)
684 db.appendURL(prefs.pimpDatabase)
685
Jack Jansenc4b217d2003-02-10 13:38:44 +0000686 if mode == 'dump':
687 db.dump(sys.stdout)
688 elif mode =='list':
Jack Jansen95839b82003-02-09 23:10:20 +0000689 if not args:
690 args = db.listnames()
691 print "%-20.20s\t%s" % ("Package", "Description")
692 print
693 for pkgname in args:
694 pkg = db.find(pkgname)
695 if pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000696 description = pkg.description()
697 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000698 else:
699 description = 'Error: no such package'
700 print "%-20.20s\t%s" % (pkgname, description)
701 if verbose:
Jack Jansene7b33db2003-02-11 22:40:59 +0000702 print "\tHome page:\t", pkg.homepage()
703 print "\tDownload URL:\t", pkg.downloadURL()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000704 elif mode =='status':
Jack Jansen95839b82003-02-09 23:10:20 +0000705 if not args:
706 args = db.listnames()
707 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
708 print
709 for pkgname in args:
710 pkg = db.find(pkgname)
711 if pkg:
712 status, msg = pkg.installed()
Jack Jansene7b33db2003-02-11 22:40:59 +0000713 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000714 else:
715 status = 'error'
716 msg = 'No such package'
717 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
718 if verbose and status == "no":
719 prereq = pkg.prerequisites()
720 for pkg, msg in prereq:
721 if not pkg:
722 pkg = ''
723 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000724 pkg = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000725 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
726 elif mode == 'install':
727 if not args:
728 print 'Please specify packages to install'
729 sys.exit(1)
730 inst = PimpInstaller(db)
731 for pkgname in args:
732 pkg = db.find(pkgname)
733 if not pkg:
734 print '%s: No such package' % pkgname
735 continue
736 list, messages = inst.prepareInstall(pkg, force)
737 if messages and not force:
738 print "%s: Not installed:" % pkgname
739 for m in messages:
740 print "\t", m
741 else:
742 if verbose:
743 output = sys.stdout
744 else:
745 output = None
746 messages = inst.install(list, output)
747 if messages:
748 print "%s: Not installed:" % pkgname
749 for m in messages:
750 print "\t", m
751
752def main():
Jack Jansen6a600ab2003-02-10 15:55:51 +0000753 """Minimal commandline tool to drive pimp."""
754
Jack Jansen95839b82003-02-09 23:10:20 +0000755 import getopt
756 def _help():
757 print "Usage: pimp [-v] -s [package ...] List installed status"
758 print " pimp [-v] -l [package ...] Show package information"
759 print " pimp [-vf] -i package ... Install packages"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000760 print " pimp -d Dump database to stdout"
Jack Jansen95839b82003-02-09 23:10:20 +0000761 print "Options:"
762 print " -v Verbose"
763 print " -f Force installation"
764 sys.exit(1)
765
766 try:
Jack Jansenc4b217d2003-02-10 13:38:44 +0000767 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
Jack Jansen95839b82003-02-09 23:10:20 +0000768 except getopt.Error:
769 _help()
770 if not opts and not args:
771 _help()
772 mode = None
773 force = 0
774 verbose = 0
775 for o, a in opts:
776 if o == '-s':
777 if mode:
778 _help()
779 mode = 'status'
780 if o == '-l':
781 if mode:
782 _help()
783 mode = 'list'
Jack Jansenc4b217d2003-02-10 13:38:44 +0000784 if o == '-d':
Jack Jansen95839b82003-02-09 23:10:20 +0000785 if mode:
786 _help()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000787 mode = 'dump'
Jack Jansen95839b82003-02-09 23:10:20 +0000788 if o == '-i':
789 mode = 'install'
790 if o == '-f':
791 force = 1
792 if o == '-v':
793 verbose = 1
794 if not mode:
795 _help()
796 _run(mode, verbose, force, args)
797
798if __name__ == '__main__':
799 main()
800
801