blob: fec040168047b756e9ed706e1e99e374fc23e060 [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 Jansen95839b82003-02-09 23:10:20 +000049]
50
Jack Jansenc4b217d2003-02-10 13:38:44 +000051class MyURLopener(urllib.FancyURLopener):
Jack Jansen8d326b82003-02-10 16:08:17 +000052 """Like FancyURLOpener, but we do want to get errors as exceptions."""
Jack Jansenc4b217d2003-02-10 13:38:44 +000053 def http_error_default(self, url, fp, errcode, errmsg, headers):
54 urllib.URLopener.http_error_default(self, url, fp, errcode, errmsg, headers)
55
Jack Jansen95839b82003-02-09 23:10:20 +000056class PimpPreferences:
Jack Jansen6a600ab2003-02-10 15:55:51 +000057 """Container for per-user preferences, such as the database to use
Jack Jansen8d326b82003-02-10 16:08:17 +000058 and where to install packages."""
Jack Jansen6a600ab2003-02-10 15:55:51 +000059
Jack Jansen95839b82003-02-09 23:10:20 +000060 def __init__(self,
61 flavorOrder=None,
62 downloadDir=None,
63 buildDir=None,
64 installDir=None,
65 pimpDatabase=None):
66 if not flavorOrder:
67 flavorOrder = DEFAULT_FLAVORORDER
68 if not downloadDir:
69 downloadDir = DEFAULT_DOWNLOADDIR
70 if not buildDir:
71 buildDir = DEFAULT_BUILDDIR
72 if not installDir:
73 installDir = DEFAULT_INSTALLDIR
74 if not pimpDatabase:
75 pimpDatabase = DEFAULT_PIMPDATABASE
76 self.flavorOrder = flavorOrder
77 self.downloadDir = downloadDir
78 self.buildDir = buildDir
79 self.installDir = installDir
80 self.pimpDatabase = pimpDatabase
81
82 def check(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +000083 """Check that the preferences make sense: directories exist and are
84 writable, the install directory is on sys.path, etc."""
85
Jack Jansen95839b82003-02-09 23:10:20 +000086 rv = ""
87 RWX_OK = os.R_OK|os.W_OK|os.X_OK
88 if not os.path.exists(self.downloadDir):
89 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
90 elif not os.access(self.downloadDir, RWX_OK):
91 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
92 if not os.path.exists(self.buildDir):
93 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
94 elif not os.access(self.buildDir, RWX_OK):
95 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
96 if not os.path.exists(self.installDir):
97 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
98 elif not os.access(self.installDir, RWX_OK):
99 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
100 else:
101 installDir = os.path.realpath(self.installDir)
102 for p in sys.path:
103 try:
104 realpath = os.path.realpath(p)
105 except:
106 pass
107 if installDir == realpath:
108 break
109 else:
110 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
111 return rv
112
113 def compareFlavors(self, left, right):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000114 """Compare two flavor strings. This is part of your preferences
115 because whether the user prefers installing from source or binary is."""
Jack Jansen95839b82003-02-09 23:10:20 +0000116 if left in self.flavorOrder:
117 if right in self.flavorOrder:
118 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
119 return -1
120 if right in self.flavorOrder:
121 return 1
122 return cmp(left, right)
123
124class PimpDatabase:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000125 """Class representing a pimp database. It can actually contain
126 information from multiple databases through inclusion, but the
127 toplevel database is considered the master, as its maintainer is
Jack Jansen8d326b82003-02-10 16:08:17 +0000128 "responsible" for the contents."""
Jack Jansen6a600ab2003-02-10 15:55:51 +0000129
Jack Jansen95839b82003-02-09 23:10:20 +0000130 def __init__(self, prefs):
131 self._packages = []
132 self.preferences = prefs
133 self._urllist = []
134 self._version = ""
135 self._maintainer = ""
136 self._description = ""
137
Jack Jansen0dacac42003-02-14 14:11:59 +0000138 def close(self):
139 """Clean up"""
140 self._packages = []
141 self.preferences = None
142
Jack Jansen95839b82003-02-09 23:10:20 +0000143 def appendURL(self, url, included=0):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000144 """Append packages from the database with the given URL.
145 Only the first database should specify included=0, so the
146 global information (maintainer, description) get stored."""
147
Jack Jansen95839b82003-02-09 23:10:20 +0000148 if url in self._urllist:
149 return
150 self._urllist.append(url)
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000151 fp = MyURLopener().open(url).fp
Jack Jansen95839b82003-02-09 23:10:20 +0000152 dict = plistlib.Plist.fromFile(fp)
153 # Test here for Pimp version, etc
154 if not included:
Jack Jansene7b33db2003-02-11 22:40:59 +0000155 self._version = dict.get('Version', '0.1')
156 if self._version != PIMP_VERSION:
157 sys.stderr.write("Warning: database version %s does not match %s\n"
158 % (self._version, PIMP_VERSION))
159 self._maintainer = dict.get('Maintainer', '')
160 self._description = dict.get('Description', '')
161 self._appendPackages(dict['Packages'])
162 others = dict.get('Include', [])
Jack Jansen95839b82003-02-09 23:10:20 +0000163 for url in others:
164 self.appendURL(url, included=1)
165
Jack Jansen6a600ab2003-02-10 15:55:51 +0000166 def _appendPackages(self, packages):
167 """Given a list of dictionaries containing package
168 descriptions create the PimpPackage objects and append them
169 to our internal storage."""
170
Jack Jansen95839b82003-02-09 23:10:20 +0000171 for p in packages:
Jack Jansen0dacac42003-02-14 14:11:59 +0000172 p = dict(p)
173 flavor = p.get('Flavor')
174 if flavor == 'source':
175 pkg = PimpPackage_source(self, p)
176 elif flavor == 'binary':
177 pkg = PimpPackage_binary(self, p)
178 else:
179 pkg = PimpPackage(self, dict(p))
Jack Jansen95839b82003-02-09 23:10:20 +0000180 self._packages.append(pkg)
181
182 def list(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000183 """Return a list of all PimpPackage objects in the database."""
184
Jack Jansen95839b82003-02-09 23:10:20 +0000185 return self._packages
186
187 def listnames(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000188 """Return a list of names of all packages in the database."""
189
Jack Jansen95839b82003-02-09 23:10:20 +0000190 rv = []
191 for pkg in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000192 rv.append(pkg.fullname())
Jack Jansen0dacac42003-02-14 14:11:59 +0000193 rv.sort()
Jack Jansen95839b82003-02-09 23:10:20 +0000194 return rv
195
196 def dump(self, pathOrFile):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000197 """Dump the contents of the database to an XML .plist file.
198
199 The file can be passed as either a file object or a pathname.
200 All data, including included databases, is dumped."""
201
Jack Jansen95839b82003-02-09 23:10:20 +0000202 packages = []
203 for pkg in self._packages:
204 packages.append(pkg.dump())
205 dict = {
Jack Jansene7b33db2003-02-11 22:40:59 +0000206 'Version': self._version,
207 'Maintainer': self._maintainer,
208 'Description': self._description,
209 'Packages': packages
Jack Jansen95839b82003-02-09 23:10:20 +0000210 }
211 plist = plistlib.Plist(**dict)
212 plist.write(pathOrFile)
213
214 def find(self, ident):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000215 """Find a package. The package can be specified by name
216 or as a dictionary with name, version and flavor entries.
217
218 Only name is obligatory. If there are multiple matches the
219 best one (higher version number, flavors ordered according to
220 users' preference) is returned."""
221
Jack Jansen95839b82003-02-09 23:10:20 +0000222 if type(ident) == str:
223 # Remove ( and ) for pseudo-packages
224 if ident[0] == '(' and ident[-1] == ')':
225 ident = ident[1:-1]
226 # Split into name-version-flavor
227 fields = ident.split('-')
228 if len(fields) < 1 or len(fields) > 3:
229 return None
230 name = fields[0]
231 if len(fields) > 1:
232 version = fields[1]
233 else:
234 version = None
235 if len(fields) > 2:
236 flavor = fields[2]
237 else:
238 flavor = None
239 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000240 name = ident['Name']
241 version = ident.get('Version')
242 flavor = ident.get('Flavor')
Jack Jansen95839b82003-02-09 23:10:20 +0000243 found = None
244 for p in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000245 if name == p.name() and \
246 (not version or version == p.version()) and \
247 (not flavor or flavor == p.flavor()):
Jack Jansen95839b82003-02-09 23:10:20 +0000248 if not found or found < p:
249 found = p
250 return found
251
Jack Jansene7b33db2003-02-11 22:40:59 +0000252ALLOWED_KEYS = [
253 "Name",
254 "Version",
255 "Flavor",
256 "Description",
257 "Home-page",
258 "Download-URL",
259 "Install-test",
260 "Install-command",
261 "Pre-install-command",
262 "Post-install-command",
263 "Prerequisites",
264 "MD5Sum"
265]
266
Jack Jansen95839b82003-02-09 23:10:20 +0000267class PimpPackage:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000268 """Class representing a single package."""
269
Jack Jansene7b33db2003-02-11 22:40:59 +0000270 def __init__(self, db, dict):
Jack Jansen95839b82003-02-09 23:10:20 +0000271 self._db = db
Jack Jansene7b33db2003-02-11 22:40:59 +0000272 name = dict["Name"]
273 for k in dict.keys():
274 if not k in ALLOWED_KEYS:
275 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
276 self._dict = dict
277
278 def __getitem__(self, key):
279 return self._dict[key]
Jack Jansen95839b82003-02-09 23:10:20 +0000280
Jack Jansene7b33db2003-02-11 22:40:59 +0000281 def name(self): return self._dict['Name']
282 def version(self): return self._dict['Version']
283 def flavor(self): return self._dict['Flavor']
284 def description(self): return self._dict['Description']
Jack Jansen0dacac42003-02-14 14:11:59 +0000285 def homepage(self): return self._dict.get('Home-page')
Jack Jansene7b33db2003-02-11 22:40:59 +0000286 def downloadURL(self): return self._dict['Download-URL']
287
288 def fullname(self):
289 """Return the full name "name-version-flavor" of a package.
290
291 If the package is a pseudo-package, something that cannot be
292 installed through pimp, return the name in (parentheses)."""
293
294 rv = self._dict['Name']
295 if self._dict.has_key('Version'):
296 rv = rv + '-%s' % self._dict['Version']
297 if self._dict.has_key('Flavor'):
298 rv = rv + '-%s' % self._dict['Flavor']
299 if not self._dict.get('Download-URL'):
300 # Pseudo-package, show in parentheses
301 rv = '(%s)' % rv
302 return rv
303
Jack Jansen95839b82003-02-09 23:10:20 +0000304 def dump(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000305 """Return a dict object containing the information on the package."""
Jack Jansene7b33db2003-02-11 22:40:59 +0000306 return self._dict
Jack Jansen95839b82003-02-09 23:10:20 +0000307
308 def __cmp__(self, other):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000309 """Compare two packages, where the "better" package sorts lower."""
310
Jack Jansen95839b82003-02-09 23:10:20 +0000311 if not isinstance(other, PimpPackage):
312 return cmp(id(self), id(other))
Jack Jansene7b33db2003-02-11 22:40:59 +0000313 if self.name() != other.name():
314 return cmp(self.name(), other.name())
315 if self.version() != other.version():
316 return -cmp(self.version(), other.version())
317 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Jack Jansen95839b82003-02-09 23:10:20 +0000318
319 def installed(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000320 """Test wheter the package is installed.
321
322 Returns two values: a status indicator which is one of
323 "yes", "no", "old" (an older version is installed) or "bad"
324 (something went wrong during the install test) and a human
325 readable string which may contain more details."""
326
Jack Jansen95839b82003-02-09 23:10:20 +0000327 namespace = {
328 "NotInstalled": _scriptExc_NotInstalled,
329 "OldInstalled": _scriptExc_OldInstalled,
330 "BadInstalled": _scriptExc_BadInstalled,
331 "os": os,
332 "sys": sys,
333 }
Jack Jansene7b33db2003-02-11 22:40:59 +0000334 installTest = self._dict['Install-test'].strip() + '\n'
Jack Jansen95839b82003-02-09 23:10:20 +0000335 try:
336 exec installTest in namespace
337 except ImportError, arg:
338 return "no", str(arg)
339 except _scriptExc_NotInstalled, arg:
340 return "no", str(arg)
341 except _scriptExc_OldInstalled, arg:
342 return "old", str(arg)
343 except _scriptExc_BadInstalled, arg:
344 return "bad", str(arg)
345 except:
Jack Jansen95839b82003-02-09 23:10:20 +0000346 return "bad", "Package install test got exception"
347 return "yes", ""
348
349 def prerequisites(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000350 """Return a list of prerequisites for this package.
351
352 The list contains 2-tuples, of which the first item is either
353 a PimpPackage object or None, and the second is a descriptive
354 string. The first item can be None if this package depends on
355 something that isn't pimp-installable, in which case the descriptive
356 string should tell the user what to do."""
357
Jack Jansen95839b82003-02-09 23:10:20 +0000358 rv = []
Jack Jansen53b341f2003-02-12 15:36:25 +0000359 if not self._dict.get('Download-URL'):
Jack Jansen95839b82003-02-09 23:10:20 +0000360 return [(None, "This package needs to be installed manually")]
Jack Jansen0dacac42003-02-14 14:11:59 +0000361 if not self._dict.get('Prerequisites'):
Jack Jansen95839b82003-02-09 23:10:20 +0000362 return []
Jack Jansene7b33db2003-02-11 22:40:59 +0000363 for item in self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000364 if type(item) == str:
365 pkg = None
366 descr = str(item)
367 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000368 name = item['Name']
369 if item.has_key('Version'):
370 name = name + '-' + item['Version']
371 if item.has_key('Flavor'):
372 name = name + '-' + item['Flavor']
373 pkg = self._db.find(name)
Jack Jansen95839b82003-02-09 23:10:20 +0000374 if not pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000375 descr = "Requires unknown %s"%name
Jack Jansen95839b82003-02-09 23:10:20 +0000376 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000377 descr = pkg.description()
Jack Jansen95839b82003-02-09 23:10:20 +0000378 rv.append((pkg, descr))
379 return rv
380
381 def _cmd(self, output, dir, *cmditems):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000382 """Internal routine to run a shell command in a given directory."""
383
Jack Jansen95839b82003-02-09 23:10:20 +0000384 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
385 if output:
386 output.write("+ %s\n" % cmd)
387 if NO_EXECUTE:
388 return 0
Jack Jansen53b341f2003-02-12 15:36:25 +0000389 dummy, fp = os.popen4(cmd, "r")
390 dummy.close()
Jack Jansen95839b82003-02-09 23:10:20 +0000391 while 1:
392 line = fp.readline()
393 if not line:
394 break
395 if output:
396 output.write(line)
397 rv = fp.close()
398 return rv
399
Jack Jansen0dacac42003-02-14 14:11:59 +0000400 def downloadPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000401 """Download a single package, if needed.
402
403 An MD5 signature is used to determine whether download is needed,
404 and to test that we actually downloaded what we expected.
405 If output is given it is a file-like object that will receive a log
406 of what happens.
407
408 If anything unforeseen happened the method returns an error message
409 string.
410 """
411
Jack Jansene7b33db2003-02-11 22:40:59 +0000412 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
Jack Jansen95839b82003-02-09 23:10:20 +0000413 path = urllib.url2pathname(path)
414 filename = os.path.split(path)[1]
Jack Jansenc4b217d2003-02-10 13:38:44 +0000415 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
416 if not self._archiveOK():
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000417 if scheme == 'manual':
418 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansenc4b217d2003-02-10 13:38:44 +0000419 if self._cmd(output, self._db.preferences.downloadDir,
420 "curl",
421 "--output", self.archiveFilename,
Jack Jansene7b33db2003-02-11 22:40:59 +0000422 self._dict['Download-URL']):
Jack Jansenc4b217d2003-02-10 13:38:44 +0000423 return "download command failed"
Jack Jansen95839b82003-02-09 23:10:20 +0000424 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
425 return "archive not found after download"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000426 if not self._archiveOK():
427 return "archive does not have correct MD5 checksum"
428
429 def _archiveOK(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000430 """Test an archive. It should exist and the MD5 checksum should be correct."""
431
Jack Jansenc4b217d2003-02-10 13:38:44 +0000432 if not os.path.exists(self.archiveFilename):
433 return 0
Jack Jansene71b9f82003-02-12 16:37:00 +0000434 if not self._dict.get('MD5Sum'):
Jack Jansene7b33db2003-02-11 22:40:59 +0000435 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
Jack Jansenc4b217d2003-02-10 13:38:44 +0000436 return 1
437 data = open(self.archiveFilename, 'rb').read()
438 checksum = md5.new(data).hexdigest()
Jack Jansene7b33db2003-02-11 22:40:59 +0000439 return checksum == self._dict['MD5Sum']
Jack Jansen95839b82003-02-09 23:10:20 +0000440
Jack Jansen0dacac42003-02-14 14:11:59 +0000441 def unpackPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000442 """Unpack a downloaded package archive."""
443
Jack Jansen95839b82003-02-09 23:10:20 +0000444 filename = os.path.split(self.archiveFilename)[1]
445 for ext, cmd in ARCHIVE_FORMATS:
446 if filename[-len(ext):] == ext:
447 break
448 else:
449 return "unknown extension for archive file: %s" % filename
450 basename = filename[:-len(ext)]
451 cmd = cmd % self.archiveFilename
Jack Jansen95839b82003-02-09 23:10:20 +0000452 if self._cmd(output, self._db.preferences.buildDir, cmd):
453 return "unpack command failed"
Jack Jansen0dacac42003-02-14 14:11:59 +0000454
455 def installPackageOnly(self, output=None):
456 """Default install method, to be overridden by subclasses"""
457 return "Cannot automatically install package %s" % self.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000458
Jack Jansen6a600ab2003-02-10 15:55:51 +0000459 def installSinglePackage(self, output=None):
460 """Download, unpack and install a single package.
461
462 If output is given it should be a file-like object and it
463 will receive a log of what happened."""
464
Jack Jansene7b33db2003-02-11 22:40:59 +0000465 if not self._dict['Download-URL']:
Jack Jansen95839b82003-02-09 23:10:20 +0000466 return "%s: This package needs to be installed manually" % _fmtpackagename(self)
Jack Jansen0dacac42003-02-14 14:11:59 +0000467 msg = self.downloadPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000468 if msg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000469 return "download %s: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000470
Jack Jansen0dacac42003-02-14 14:11:59 +0000471 msg = self.unpackPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000472 if msg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000473 return "unpack %s: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000474
Jack Jansen0dacac42003-02-14 14:11:59 +0000475 return self.installPackageOnly(output)
Jack Jansen53b341f2003-02-12 15:36:25 +0000476
Jack Jansen0dacac42003-02-14 14:11:59 +0000477 def beforeInstall(self):
478 """Bookkeeping before installation: remember what we have in site-packages"""
479 self._old_contents = os.listdir(self._db.preferences.installDir)
480
481 def afterInstall(self):
482 """Bookkeeping after installation: interpret any new .pth files that have
483 appeared"""
484
Jack Jansen53b341f2003-02-12 15:36:25 +0000485 new_contents = os.listdir(self._db.preferences.installDir)
Jack Jansen53b341f2003-02-12 15:36:25 +0000486 for fn in new_contents:
Jack Jansen0dacac42003-02-14 14:11:59 +0000487 if fn in self._old_contents:
Jack Jansen53b341f2003-02-12 15:36:25 +0000488 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:
Jack Jansen0dacac42003-02-14 14:11:59 +0000507 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000508
Jack Jansen0dacac42003-02-14 14:11:59 +0000509class PimpPackage_binary(PimpPackage):
510
511 def unpackPackageOnly(self, output=None):
512 """We don't unpack binary packages until installing"""
513 pass
514
515 def installPackageOnly(self, output=None):
516 """Install a single source package.
517
518 If output is given it should be a file-like object and it
519 will receive a log of what happened."""
520
521 msgs = []
522 if self._dict.has_key('Pre-install-command'):
523 msg.append("%s: Pre-install-command ignored" % self.fullname())
524 if self._dict.has_key('Install-command'):
525 msgs.append("%s: Install-command ignored" % self.fullname())
526 if self._dict.has_key('Post-install-command'):
527 msgs.append("%s: Post-install-command ignored" % self.fullname())
528
529 self.beforeInstall()
530
531 # Install by unpacking
532 filename = os.path.split(self.archiveFilename)[1]
533 for ext, cmd in ARCHIVE_FORMATS:
534 if filename[-len(ext):] == ext:
535 break
536 else:
537 return "unknown extension for archive file: %s" % filename
538
539 # Modify where the files are extracted
540 prefixmod = '-C /'
541 cmd = cmd % self.archiveFilename
542 if self._cmd(output, self._db.preferences.buildDir, cmd, prefixmod):
543 return "unpack command failed"
544
545 self.afterInstall()
546
547 if self._dict.has_key('Post-install-command'):
548 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
549 return "post-install %s: running \"%s\" failed" % \
550 (self.fullname(), self._dict['Post-install-command'])
551 return None
552
553
554class PimpPackage_source(PimpPackage):
555
556 def unpackPackageOnly(self, output=None):
557 """Unpack a source package and check that setup.py exists"""
558 PimpPackage.unpackPackageOnly(self, output)
559 # Test that a setup script has been create
560 self._buildDirname = os.path.join(self._db.preferences.buildDir, basename)
561 setupname = os.path.join(self._buildDirname, "setup.py")
562 if not os.path.exists(setupname) and not NO_EXECUTE:
563 return "no setup.py found after unpack of archive"
564
565 def installPackageOnly(self, output=None):
566 """Install a single source package.
567
568 If output is given it should be a file-like object and it
569 will receive a log of what happened."""
570
571 if self._dict.has_key('Pre-install-command'):
572 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
573 return "pre-install %s: running \"%s\" failed" % \
574 (self.fullname(), self._dict['Pre-install-command'])
575
576 self.beforeInstall()
577 installcmd = self._dict.get('Install-command')
578 if not installcmd:
579 installcmd = '"%s" setup.py install' % sys.executable
580 if self._cmd(output, self._buildDirname, installcmd):
581 return "install %s: running \"%s\" failed" % self.fullname()
582
583 self.afterInstall()
584
585 if self._dict.has_key('Post-install-command'):
586 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
587 return "post-install %s: running \"%s\" failed" % \
588 (self.fullname(), self._dict['Post-install-command'])
589 return None
590
591
Jack Jansen95839b82003-02-09 23:10:20 +0000592class PimpInstaller:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000593 """Installer engine: computes dependencies and installs
594 packages in the right order."""
595
Jack Jansen95839b82003-02-09 23:10:20 +0000596 def __init__(self, db):
597 self._todo = []
598 self._db = db
599 self._curtodo = []
600 self._curmessages = []
601
602 def __contains__(self, package):
603 return package in self._todo
604
605 def _addPackages(self, packages):
606 for package in packages:
607 if not package in self._todo:
608 self._todo.insert(0, package)
609
610 def _prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000611 """Internal routine, recursive engine for prepareInstall.
612
613 Test whether the package is installed and (if not installed
614 or if force==1) prepend it to the temporary todo list and
615 call ourselves recursively on all prerequisites."""
616
Jack Jansen95839b82003-02-09 23:10:20 +0000617 if not force:
618 status, message = package.installed()
619 if status == "yes":
620 return
621 if package in self._todo or package in self._curtodo:
622 return
623 self._curtodo.insert(0, package)
624 if not recursive:
625 return
626 prereqs = package.prerequisites()
627 for pkg, descr in prereqs:
628 if pkg:
629 self._prepareInstall(pkg, force, recursive)
630 else:
631 self._curmessages.append("Requires: %s" % descr)
632
633 def prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000634 """Prepare installation of a package.
635
636 If the package is already installed and force is false nothing
637 is done. If recursive is true prerequisites are installed first.
638
639 Returns a list of packages (to be passed to install) and a list
640 of messages of any problems encountered.
641 """
642
Jack Jansen95839b82003-02-09 23:10:20 +0000643 self._curtodo = []
644 self._curmessages = []
645 self._prepareInstall(package, force, recursive)
646 rv = self._curtodo, self._curmessages
647 self._curtodo = []
648 self._curmessages = []
649 return rv
650
651 def install(self, packages, output):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000652 """Install a list of packages."""
653
Jack Jansen95839b82003-02-09 23:10:20 +0000654 self._addPackages(packages)
655 status = []
656 for pkg in self._todo:
657 msg = pkg.installSinglePackage(output)
658 if msg:
659 status.append(msg)
660 return status
661
662
Jack Jansen95839b82003-02-09 23:10:20 +0000663
664def _run(mode, verbose, force, args):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000665 """Engine for the main program"""
666
Jack Jansen95839b82003-02-09 23:10:20 +0000667 prefs = PimpPreferences()
668 prefs.check()
669 db = PimpDatabase(prefs)
670 db.appendURL(prefs.pimpDatabase)
671
Jack Jansenc4b217d2003-02-10 13:38:44 +0000672 if mode == 'dump':
673 db.dump(sys.stdout)
674 elif mode =='list':
Jack Jansen95839b82003-02-09 23:10:20 +0000675 if not args:
676 args = db.listnames()
677 print "%-20.20s\t%s" % ("Package", "Description")
678 print
679 for pkgname in args:
680 pkg = db.find(pkgname)
681 if pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000682 description = pkg.description()
683 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000684 else:
685 description = 'Error: no such package'
686 print "%-20.20s\t%s" % (pkgname, description)
687 if verbose:
Jack Jansene7b33db2003-02-11 22:40:59 +0000688 print "\tHome page:\t", pkg.homepage()
689 print "\tDownload URL:\t", pkg.downloadURL()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000690 elif mode =='status':
Jack Jansen95839b82003-02-09 23:10:20 +0000691 if not args:
692 args = db.listnames()
693 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
694 print
695 for pkgname in args:
696 pkg = db.find(pkgname)
697 if pkg:
698 status, msg = pkg.installed()
Jack Jansene7b33db2003-02-11 22:40:59 +0000699 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000700 else:
701 status = 'error'
702 msg = 'No such package'
703 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
704 if verbose and status == "no":
705 prereq = pkg.prerequisites()
706 for pkg, msg in prereq:
707 if not pkg:
708 pkg = ''
709 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000710 pkg = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000711 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
712 elif mode == 'install':
713 if not args:
714 print 'Please specify packages to install'
715 sys.exit(1)
716 inst = PimpInstaller(db)
717 for pkgname in args:
718 pkg = db.find(pkgname)
719 if not pkg:
720 print '%s: No such package' % pkgname
721 continue
722 list, messages = inst.prepareInstall(pkg, force)
723 if messages and not force:
724 print "%s: Not installed:" % pkgname
725 for m in messages:
726 print "\t", m
727 else:
728 if verbose:
729 output = sys.stdout
730 else:
731 output = None
732 messages = inst.install(list, output)
733 if messages:
734 print "%s: Not installed:" % pkgname
735 for m in messages:
736 print "\t", m
737
738def main():
Jack Jansen6a600ab2003-02-10 15:55:51 +0000739 """Minimal commandline tool to drive pimp."""
740
Jack Jansen95839b82003-02-09 23:10:20 +0000741 import getopt
742 def _help():
743 print "Usage: pimp [-v] -s [package ...] List installed status"
744 print " pimp [-v] -l [package ...] Show package information"
745 print " pimp [-vf] -i package ... Install packages"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000746 print " pimp -d Dump database to stdout"
Jack Jansen95839b82003-02-09 23:10:20 +0000747 print "Options:"
748 print " -v Verbose"
749 print " -f Force installation"
750 sys.exit(1)
751
752 try:
Jack Jansenc4b217d2003-02-10 13:38:44 +0000753 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
Jack Jansen95839b82003-02-09 23:10:20 +0000754 except getopt.Error:
755 _help()
756 if not opts and not args:
757 _help()
758 mode = None
759 force = 0
760 verbose = 0
761 for o, a in opts:
762 if o == '-s':
763 if mode:
764 _help()
765 mode = 'status'
766 if o == '-l':
767 if mode:
768 _help()
769 mode = 'list'
Jack Jansenc4b217d2003-02-10 13:38:44 +0000770 if o == '-d':
Jack Jansen95839b82003-02-09 23:10:20 +0000771 if mode:
772 _help()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000773 mode = 'dump'
Jack Jansen95839b82003-02-09 23:10:20 +0000774 if o == '-i':
775 mode = 'install'
776 if o == '-f':
777 force = 1
778 if o == '-v':
779 verbose = 1
780 if not mode:
781 _help()
782 _run(mode, verbose, force, args)
783
784if __name__ == '__main__':
785 main()
786
787