blob: 6664475d4256febb78030dbbcc809e617718b4b1 [file] [log] [blame]
Jack Jansen6a600ab2003-02-10 15:55:51 +00001"""Package Install Manager for Python.
2
3This is currently a MacOSX-only strawman implementation.
4Motto: "He may be shabby, but he gets you what you need" :-)
5
6Tools to allow easy installation of packages. The idea is that there is
7an online XML database per (platform, python-version) containing packages
8known to work with that combination. This module contains tools for getting
9and parsing the database, testing whether packages are installed, computing
10dependencies and installing packages.
11
12There is a minimal main program that works as a command line tool, but the
13intention is that the end user will use this through a GUI.
14"""
Jack Jansen95839b82003-02-09 23:10:20 +000015import sys
16import os
Jack Jansen450bd872003-03-17 10:54:41 +000017import popen2
Jack Jansen95839b82003-02-09 23:10:20 +000018import urllib
Jack Jansen47e59872003-03-11 14:37:19 +000019import urllib2
Jack Jansen95839b82003-02-09 23:10:20 +000020import urlparse
21import plistlib
22import distutils.util
Jack Jansene71b9f82003-02-12 16:37:00 +000023import distutils.sysconfig
Jack Jansenc4b217d2003-02-10 13:38:44 +000024import md5
Jack Jansen95839b82003-02-09 23:10:20 +000025
Jack Jansen6a600ab2003-02-10 15:55:51 +000026__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main"]
27
Jack Jansen95839b82003-02-09 23:10:20 +000028_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
29_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
30_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
31
32NO_EXECUTE=0
33
Jack Jansene7b33db2003-02-11 22:40:59 +000034PIMP_VERSION="0.1"
35
Jack Jansen0dacac42003-02-14 14:11:59 +000036# Flavors:
37# source: setup-based package
38# binary: tar (or other) archive created with setup.py bdist.
Jack Jansen95839b82003-02-09 23:10:20 +000039DEFAULT_FLAVORORDER=['source', 'binary']
40DEFAULT_DOWNLOADDIR='/tmp'
41DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000042DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansen95839b82003-02-09 23:10:20 +000043DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
44
45ARCHIVE_FORMATS = [
Jack Jansen0dacac42003-02-14 14:11:59 +000046 (".tar.Z", "zcat \"%s\" | tar -xf -"),
47 (".taz", "zcat \"%s\" | tar -xf -"),
48 (".tar.gz", "zcat \"%s\" | tar -xf -"),
49 (".tgz", "zcat \"%s\" | tar -xf -"),
50 (".tar.bz", "bzcat \"%s\" | tar -xf -"),
Jack Jansena460f442003-02-17 12:21:05 +000051 (".zip", "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +000052]
53
54class PimpPreferences:
Jack Jansen6a600ab2003-02-10 15:55:51 +000055 """Container for per-user preferences, such as the database to use
Jack Jansen8d326b82003-02-10 16:08:17 +000056 and where to install packages."""
Jack Jansen6a600ab2003-02-10 15:55:51 +000057
Jack Jansen95839b82003-02-09 23:10:20 +000058 def __init__(self,
59 flavorOrder=None,
60 downloadDir=None,
61 buildDir=None,
62 installDir=None,
63 pimpDatabase=None):
64 if not flavorOrder:
65 flavorOrder = DEFAULT_FLAVORORDER
66 if not downloadDir:
67 downloadDir = DEFAULT_DOWNLOADDIR
68 if not buildDir:
69 buildDir = DEFAULT_BUILDDIR
70 if not installDir:
71 installDir = DEFAULT_INSTALLDIR
72 if not pimpDatabase:
73 pimpDatabase = DEFAULT_PIMPDATABASE
74 self.flavorOrder = flavorOrder
75 self.downloadDir = downloadDir
76 self.buildDir = buildDir
77 self.installDir = installDir
78 self.pimpDatabase = pimpDatabase
79
80 def check(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +000081 """Check that the preferences make sense: directories exist and are
82 writable, the install directory is on sys.path, etc."""
83
Jack Jansen95839b82003-02-09 23:10:20 +000084 rv = ""
85 RWX_OK = os.R_OK|os.W_OK|os.X_OK
86 if not os.path.exists(self.downloadDir):
87 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
88 elif not os.access(self.downloadDir, RWX_OK):
89 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
90 if not os.path.exists(self.buildDir):
91 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
92 elif not os.access(self.buildDir, RWX_OK):
93 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
94 if not os.path.exists(self.installDir):
95 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
96 elif not os.access(self.installDir, RWX_OK):
97 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
98 else:
99 installDir = os.path.realpath(self.installDir)
100 for p in sys.path:
101 try:
102 realpath = os.path.realpath(p)
103 except:
104 pass
105 if installDir == realpath:
106 break
107 else:
108 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
109 return rv
110
111 def compareFlavors(self, left, right):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000112 """Compare two flavor strings. This is part of your preferences
113 because whether the user prefers installing from source or binary is."""
Jack Jansen95839b82003-02-09 23:10:20 +0000114 if left in self.flavorOrder:
115 if right in self.flavorOrder:
116 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
117 return -1
118 if right in self.flavorOrder:
119 return 1
120 return cmp(left, right)
121
122class PimpDatabase:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000123 """Class representing a pimp database. It can actually contain
124 information from multiple databases through inclusion, but the
125 toplevel database is considered the master, as its maintainer is
Jack Jansen8d326b82003-02-10 16:08:17 +0000126 "responsible" for the contents."""
Jack Jansen6a600ab2003-02-10 15:55:51 +0000127
Jack Jansen95839b82003-02-09 23:10:20 +0000128 def __init__(self, prefs):
129 self._packages = []
130 self.preferences = prefs
131 self._urllist = []
132 self._version = ""
133 self._maintainer = ""
134 self._description = ""
135
Jack Jansen0dacac42003-02-14 14:11:59 +0000136 def close(self):
137 """Clean up"""
138 self._packages = []
139 self.preferences = None
140
Jack Jansen95839b82003-02-09 23:10:20 +0000141 def appendURL(self, url, included=0):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000142 """Append packages from the database with the given URL.
143 Only the first database should specify included=0, so the
144 global information (maintainer, description) get stored."""
145
Jack Jansen95839b82003-02-09 23:10:20 +0000146 if url in self._urllist:
147 return
148 self._urllist.append(url)
Jack Jansen47e59872003-03-11 14:37:19 +0000149 fp = urllib2.urlopen(url).fp
Jack Jansen95839b82003-02-09 23:10:20 +0000150 dict = plistlib.Plist.fromFile(fp)
151 # Test here for Pimp version, etc
152 if not included:
Jack Jansene7b33db2003-02-11 22:40:59 +0000153 self._version = dict.get('Version', '0.1')
154 if self._version != PIMP_VERSION:
155 sys.stderr.write("Warning: database version %s does not match %s\n"
156 % (self._version, PIMP_VERSION))
157 self._maintainer = dict.get('Maintainer', '')
158 self._description = dict.get('Description', '')
159 self._appendPackages(dict['Packages'])
160 others = dict.get('Include', [])
Jack Jansen95839b82003-02-09 23:10:20 +0000161 for url in others:
162 self.appendURL(url, included=1)
163
Jack Jansen6a600ab2003-02-10 15:55:51 +0000164 def _appendPackages(self, packages):
165 """Given a list of dictionaries containing package
166 descriptions create the PimpPackage objects and append them
167 to our internal storage."""
168
Jack Jansen95839b82003-02-09 23:10:20 +0000169 for p in packages:
Jack Jansen0dacac42003-02-14 14:11:59 +0000170 p = dict(p)
171 flavor = p.get('Flavor')
172 if flavor == 'source':
173 pkg = PimpPackage_source(self, p)
174 elif flavor == 'binary':
175 pkg = PimpPackage_binary(self, p)
176 else:
177 pkg = PimpPackage(self, dict(p))
Jack Jansen95839b82003-02-09 23:10:20 +0000178 self._packages.append(pkg)
179
180 def list(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000181 """Return a list of all PimpPackage objects in the database."""
182
Jack Jansen95839b82003-02-09 23:10:20 +0000183 return self._packages
184
185 def listnames(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000186 """Return a list of names of all packages in the database."""
187
Jack Jansen95839b82003-02-09 23:10:20 +0000188 rv = []
189 for pkg in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000190 rv.append(pkg.fullname())
Jack Jansen0dacac42003-02-14 14:11:59 +0000191 rv.sort()
Jack Jansen95839b82003-02-09 23:10:20 +0000192 return rv
193
194 def dump(self, pathOrFile):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000195 """Dump the contents of the database to an XML .plist file.
196
197 The file can be passed as either a file object or a pathname.
198 All data, including included databases, is dumped."""
199
Jack Jansen95839b82003-02-09 23:10:20 +0000200 packages = []
201 for pkg in self._packages:
202 packages.append(pkg.dump())
203 dict = {
Jack Jansene7b33db2003-02-11 22:40:59 +0000204 'Version': self._version,
205 'Maintainer': self._maintainer,
206 'Description': self._description,
207 'Packages': packages
Jack Jansen95839b82003-02-09 23:10:20 +0000208 }
209 plist = plistlib.Plist(**dict)
210 plist.write(pathOrFile)
211
212 def find(self, ident):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000213 """Find a package. The package can be specified by name
214 or as a dictionary with name, version and flavor entries.
215
216 Only name is obligatory. If there are multiple matches the
217 best one (higher version number, flavors ordered according to
218 users' preference) is returned."""
219
Jack Jansen95839b82003-02-09 23:10:20 +0000220 if type(ident) == str:
221 # Remove ( and ) for pseudo-packages
222 if ident[0] == '(' and ident[-1] == ')':
223 ident = ident[1:-1]
224 # Split into name-version-flavor
225 fields = ident.split('-')
226 if len(fields) < 1 or len(fields) > 3:
227 return None
228 name = fields[0]
229 if len(fields) > 1:
230 version = fields[1]
231 else:
232 version = None
233 if len(fields) > 2:
234 flavor = fields[2]
235 else:
236 flavor = None
237 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000238 name = ident['Name']
239 version = ident.get('Version')
240 flavor = ident.get('Flavor')
Jack Jansen95839b82003-02-09 23:10:20 +0000241 found = None
242 for p in self._packages:
Jack Jansene7b33db2003-02-11 22:40:59 +0000243 if name == p.name() and \
244 (not version or version == p.version()) and \
245 (not flavor or flavor == p.flavor()):
Jack Jansen95839b82003-02-09 23:10:20 +0000246 if not found or found < p:
247 found = p
248 return found
249
Jack Jansene7b33db2003-02-11 22:40:59 +0000250ALLOWED_KEYS = [
251 "Name",
252 "Version",
253 "Flavor",
254 "Description",
255 "Home-page",
256 "Download-URL",
257 "Install-test",
258 "Install-command",
259 "Pre-install-command",
260 "Post-install-command",
261 "Prerequisites",
262 "MD5Sum"
263]
264
Jack Jansen95839b82003-02-09 23:10:20 +0000265class PimpPackage:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000266 """Class representing a single package."""
267
Jack Jansene7b33db2003-02-11 22:40:59 +0000268 def __init__(self, db, dict):
Jack Jansen95839b82003-02-09 23:10:20 +0000269 self._db = db
Jack Jansene7b33db2003-02-11 22:40:59 +0000270 name = dict["Name"]
271 for k in dict.keys():
272 if not k in ALLOWED_KEYS:
273 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
274 self._dict = dict
275
276 def __getitem__(self, key):
277 return self._dict[key]
Jack Jansen95839b82003-02-09 23:10:20 +0000278
Jack Jansene7b33db2003-02-11 22:40:59 +0000279 def name(self): return self._dict['Name']
280 def version(self): return self._dict['Version']
281 def flavor(self): return self._dict['Flavor']
282 def description(self): return self._dict['Description']
Jack Jansen0dacac42003-02-14 14:11:59 +0000283 def homepage(self): return self._dict.get('Home-page')
Jack Jansene7b33db2003-02-11 22:40:59 +0000284 def downloadURL(self): return self._dict['Download-URL']
285
286 def fullname(self):
287 """Return the full name "name-version-flavor" of a package.
288
289 If the package is a pseudo-package, something that cannot be
290 installed through pimp, return the name in (parentheses)."""
291
292 rv = self._dict['Name']
293 if self._dict.has_key('Version'):
294 rv = rv + '-%s' % self._dict['Version']
295 if self._dict.has_key('Flavor'):
296 rv = rv + '-%s' % self._dict['Flavor']
297 if not self._dict.get('Download-URL'):
298 # Pseudo-package, show in parentheses
299 rv = '(%s)' % rv
300 return rv
301
Jack Jansen95839b82003-02-09 23:10:20 +0000302 def dump(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000303 """Return a dict object containing the information on the package."""
Jack Jansene7b33db2003-02-11 22:40:59 +0000304 return self._dict
Jack Jansen95839b82003-02-09 23:10:20 +0000305
306 def __cmp__(self, other):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000307 """Compare two packages, where the "better" package sorts lower."""
308
Jack Jansen95839b82003-02-09 23:10:20 +0000309 if not isinstance(other, PimpPackage):
310 return cmp(id(self), id(other))
Jack Jansene7b33db2003-02-11 22:40:59 +0000311 if self.name() != other.name():
312 return cmp(self.name(), other.name())
313 if self.version() != other.version():
314 return -cmp(self.version(), other.version())
315 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Jack Jansen95839b82003-02-09 23:10:20 +0000316
317 def installed(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000318 """Test wheter the package is installed.
319
320 Returns two values: a status indicator which is one of
321 "yes", "no", "old" (an older version is installed) or "bad"
322 (something went wrong during the install test) and a human
323 readable string which may contain more details."""
324
Jack Jansen95839b82003-02-09 23:10:20 +0000325 namespace = {
326 "NotInstalled": _scriptExc_NotInstalled,
327 "OldInstalled": _scriptExc_OldInstalled,
328 "BadInstalled": _scriptExc_BadInstalled,
329 "os": os,
330 "sys": sys,
331 }
Jack Jansene7b33db2003-02-11 22:40:59 +0000332 installTest = self._dict['Install-test'].strip() + '\n'
Jack Jansen95839b82003-02-09 23:10:20 +0000333 try:
334 exec installTest in namespace
335 except ImportError, arg:
336 return "no", str(arg)
337 except _scriptExc_NotInstalled, arg:
338 return "no", str(arg)
339 except _scriptExc_OldInstalled, arg:
340 return "old", str(arg)
341 except _scriptExc_BadInstalled, arg:
342 return "bad", str(arg)
343 except:
Jack Jansena460f442003-02-17 12:21:05 +0000344 sys.stderr.write("-------------------------------------\n")
345 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
346 sys.stderr.write("---- source:\n")
347 sys.stderr.write(installTest)
348 sys.stderr.write("---- exception:\n")
349 import traceback
350 traceback.print_exc(file=sys.stderr)
351 if self._db._maintainer:
352 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
353 sys.stderr.write("-------------------------------------\n")
Jack Jansen95839b82003-02-09 23:10:20 +0000354 return "bad", "Package install test got exception"
355 return "yes", ""
356
357 def prerequisites(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000358 """Return a list of prerequisites for this package.
359
360 The list contains 2-tuples, of which the first item is either
361 a PimpPackage object or None, and the second is a descriptive
362 string. The first item can be None if this package depends on
363 something that isn't pimp-installable, in which case the descriptive
364 string should tell the user what to do."""
365
Jack Jansen95839b82003-02-09 23:10:20 +0000366 rv = []
Jack Jansen53b341f2003-02-12 15:36:25 +0000367 if not self._dict.get('Download-URL'):
Jack Jansena2125fe2003-02-16 23:03:04 +0000368 return [(None,
369 "%s: This package needs to be installed manually (no Download-URL field)" %
370 self.fullname())]
Jack Jansen0dacac42003-02-14 14:11:59 +0000371 if not self._dict.get('Prerequisites'):
Jack Jansen95839b82003-02-09 23:10:20 +0000372 return []
Jack Jansene7b33db2003-02-11 22:40:59 +0000373 for item in self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000374 if type(item) == str:
375 pkg = None
376 descr = str(item)
377 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000378 name = item['Name']
379 if item.has_key('Version'):
380 name = name + '-' + item['Version']
381 if item.has_key('Flavor'):
382 name = name + '-' + item['Flavor']
383 pkg = self._db.find(name)
Jack Jansen95839b82003-02-09 23:10:20 +0000384 if not pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000385 descr = "Requires unknown %s"%name
Jack Jansen95839b82003-02-09 23:10:20 +0000386 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000387 descr = pkg.description()
Jack Jansen95839b82003-02-09 23:10:20 +0000388 rv.append((pkg, descr))
389 return rv
390
391 def _cmd(self, output, dir, *cmditems):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000392 """Internal routine to run a shell command in a given directory."""
393
Jack Jansen95839b82003-02-09 23:10:20 +0000394 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
395 if output:
396 output.write("+ %s\n" % cmd)
397 if NO_EXECUTE:
398 return 0
Jack Jansen450bd872003-03-17 10:54:41 +0000399 child = popen2.Popen4(cmd)
400 child.tochild.close()
Jack Jansen95839b82003-02-09 23:10:20 +0000401 while 1:
Jack Jansen450bd872003-03-17 10:54:41 +0000402 line = child.fromchild.readline()
Jack Jansen95839b82003-02-09 23:10:20 +0000403 if not line:
404 break
405 if output:
406 output.write(line)
Jack Jansen450bd872003-03-17 10:54:41 +0000407 return child.wait()
Jack Jansen95839b82003-02-09 23:10:20 +0000408
Jack Jansen0dacac42003-02-14 14:11:59 +0000409 def downloadPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000410 """Download a single package, if needed.
411
412 An MD5 signature is used to determine whether download is needed,
413 and to test that we actually downloaded what we expected.
414 If output is given it is a file-like object that will receive a log
415 of what happens.
416
417 If anything unforeseen happened the method returns an error message
418 string.
419 """
420
Jack Jansene7b33db2003-02-11 22:40:59 +0000421 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
Jack Jansen95839b82003-02-09 23:10:20 +0000422 path = urllib.url2pathname(path)
423 filename = os.path.split(path)[1]
Jack Jansenc4b217d2003-02-10 13:38:44 +0000424 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
425 if not self._archiveOK():
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000426 if scheme == 'manual':
427 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansenc4b217d2003-02-10 13:38:44 +0000428 if self._cmd(output, self._db.preferences.downloadDir,
429 "curl",
430 "--output", self.archiveFilename,
Jack Jansene7b33db2003-02-11 22:40:59 +0000431 self._dict['Download-URL']):
Jack Jansenc4b217d2003-02-10 13:38:44 +0000432 return "download command failed"
Jack Jansen95839b82003-02-09 23:10:20 +0000433 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
434 return "archive not found after download"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000435 if not self._archiveOK():
436 return "archive does not have correct MD5 checksum"
437
438 def _archiveOK(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000439 """Test an archive. It should exist and the MD5 checksum should be correct."""
440
Jack Jansenc4b217d2003-02-10 13:38:44 +0000441 if not os.path.exists(self.archiveFilename):
442 return 0
Jack Jansene71b9f82003-02-12 16:37:00 +0000443 if not self._dict.get('MD5Sum'):
Jack Jansene7b33db2003-02-11 22:40:59 +0000444 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
Jack Jansenc4b217d2003-02-10 13:38:44 +0000445 return 1
446 data = open(self.archiveFilename, 'rb').read()
447 checksum = md5.new(data).hexdigest()
Jack Jansene7b33db2003-02-11 22:40:59 +0000448 return checksum == self._dict['MD5Sum']
Jack Jansen95839b82003-02-09 23:10:20 +0000449
Jack Jansen0dacac42003-02-14 14:11:59 +0000450 def unpackPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000451 """Unpack a downloaded package archive."""
452
Jack Jansen95839b82003-02-09 23:10:20 +0000453 filename = os.path.split(self.archiveFilename)[1]
454 for ext, cmd in ARCHIVE_FORMATS:
455 if filename[-len(ext):] == ext:
456 break
457 else:
458 return "unknown extension for archive file: %s" % filename
Jack Jansena2125fe2003-02-16 23:03:04 +0000459 self.basename = filename[:-len(ext)]
Jack Jansen95839b82003-02-09 23:10:20 +0000460 cmd = cmd % self.archiveFilename
Jack Jansen95839b82003-02-09 23:10:20 +0000461 if self._cmd(output, self._db.preferences.buildDir, cmd):
462 return "unpack command failed"
Jack Jansen0dacac42003-02-14 14:11:59 +0000463
464 def installPackageOnly(self, output=None):
465 """Default install method, to be overridden by subclasses"""
Jack Jansena2125fe2003-02-16 23:03:04 +0000466 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
467 % (self.fullname(), self._dict.get(flavor, ""))
Jack Jansen95839b82003-02-09 23:10:20 +0000468
Jack Jansen6a600ab2003-02-10 15:55:51 +0000469 def installSinglePackage(self, output=None):
470 """Download, unpack and install a single package.
471
472 If output is given it should be a file-like object and it
473 will receive a log of what happened."""
474
Jack Jansene7b33db2003-02-11 22:40:59 +0000475 if not self._dict['Download-URL']:
Jack Jansena2125fe2003-02-16 23:03:04 +0000476 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
Jack Jansen0dacac42003-02-14 14:11:59 +0000477 msg = self.downloadPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000478 if msg:
Jack Jansena2125fe2003-02-16 23:03:04 +0000479 return "%s: download: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000480
Jack Jansen0dacac42003-02-14 14:11:59 +0000481 msg = self.unpackPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000482 if msg:
Jack Jansena2125fe2003-02-16 23:03:04 +0000483 return "%s: unpack: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000484
Jack Jansen0dacac42003-02-14 14:11:59 +0000485 return self.installPackageOnly(output)
Jack Jansen53b341f2003-02-12 15:36:25 +0000486
Jack Jansen0dacac42003-02-14 14:11:59 +0000487 def beforeInstall(self):
488 """Bookkeeping before installation: remember what we have in site-packages"""
489 self._old_contents = os.listdir(self._db.preferences.installDir)
490
491 def afterInstall(self):
492 """Bookkeeping after installation: interpret any new .pth files that have
493 appeared"""
494
Jack Jansen53b341f2003-02-12 15:36:25 +0000495 new_contents = os.listdir(self._db.preferences.installDir)
Jack Jansen53b341f2003-02-12 15:36:25 +0000496 for fn in new_contents:
Jack Jansen0dacac42003-02-14 14:11:59 +0000497 if fn in self._old_contents:
Jack Jansen53b341f2003-02-12 15:36:25 +0000498 continue
499 if fn[-4:] != '.pth':
500 continue
501 fullname = os.path.join(self._db.preferences.installDir, fn)
502 f = open(fullname)
503 for line in f.readlines():
504 if not line:
505 continue
506 if line[0] == '#':
507 continue
508 if line[:6] == 'import':
509 exec line
510 continue
511 if line[-1] == '\n':
512 line = line[:-1]
513 if not os.path.isabs(line):
514 line = os.path.join(self._db.preferences.installDir, line)
515 line = os.path.realpath(line)
516 if not line in sys.path:
Jack Jansen0dacac42003-02-14 14:11:59 +0000517 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000518
Jack Jansen0dacac42003-02-14 14:11:59 +0000519class PimpPackage_binary(PimpPackage):
520
521 def unpackPackageOnly(self, output=None):
522 """We don't unpack binary packages until installing"""
523 pass
524
525 def installPackageOnly(self, output=None):
526 """Install a single source package.
527
528 If output is given it should be a file-like object and it
529 will receive a log of what happened."""
Jack Jansena2125fe2003-02-16 23:03:04 +0000530 print 'PimpPackage_binary installPackageOnly'
Jack Jansen0dacac42003-02-14 14:11:59 +0000531
532 msgs = []
533 if self._dict.has_key('Pre-install-command'):
534 msg.append("%s: Pre-install-command ignored" % self.fullname())
535 if self._dict.has_key('Install-command'):
536 msgs.append("%s: Install-command ignored" % self.fullname())
537 if self._dict.has_key('Post-install-command'):
538 msgs.append("%s: Post-install-command ignored" % self.fullname())
539
540 self.beforeInstall()
541
542 # Install by unpacking
543 filename = os.path.split(self.archiveFilename)[1]
544 for ext, cmd in ARCHIVE_FORMATS:
545 if filename[-len(ext):] == ext:
546 break
547 else:
548 return "unknown extension for archive file: %s" % filename
549
Jack Jansena460f442003-02-17 12:21:05 +0000550 # Extract the files in the root folder.
Jack Jansen0dacac42003-02-14 14:11:59 +0000551 cmd = cmd % self.archiveFilename
Jack Jansena460f442003-02-17 12:21:05 +0000552 if self._cmd(output, "/", cmd):
Jack Jansen0dacac42003-02-14 14:11:59 +0000553 return "unpack command failed"
554
555 self.afterInstall()
556
557 if self._dict.has_key('Post-install-command'):
558 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
559 return "post-install %s: running \"%s\" failed" % \
560 (self.fullname(), self._dict['Post-install-command'])
561 return None
562
563
564class PimpPackage_source(PimpPackage):
565
566 def unpackPackageOnly(self, output=None):
567 """Unpack a source package and check that setup.py exists"""
568 PimpPackage.unpackPackageOnly(self, output)
569 # Test that a setup script has been create
Jack Jansena2125fe2003-02-16 23:03:04 +0000570 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
Jack Jansen0dacac42003-02-14 14:11:59 +0000571 setupname = os.path.join(self._buildDirname, "setup.py")
572 if not os.path.exists(setupname) and not NO_EXECUTE:
573 return "no setup.py found after unpack of archive"
574
575 def installPackageOnly(self, output=None):
576 """Install a single source package.
577
578 If output is given it should be a file-like object and it
579 will receive a log of what happened."""
580
581 if self._dict.has_key('Pre-install-command'):
582 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
583 return "pre-install %s: running \"%s\" failed" % \
584 (self.fullname(), self._dict['Pre-install-command'])
585
586 self.beforeInstall()
587 installcmd = self._dict.get('Install-command')
588 if not installcmd:
589 installcmd = '"%s" setup.py install' % sys.executable
590 if self._cmd(output, self._buildDirname, installcmd):
Jack Jansen450bd872003-03-17 10:54:41 +0000591 return "install %s: running \"%s\" failed" % \
592 (self.fullname(), installcmd)
Jack Jansen0dacac42003-02-14 14:11:59 +0000593
594 self.afterInstall()
595
596 if self._dict.has_key('Post-install-command'):
597 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
598 return "post-install %s: running \"%s\" failed" % \
599 (self.fullname(), self._dict['Post-install-command'])
600 return None
601
602
Jack Jansen95839b82003-02-09 23:10:20 +0000603class PimpInstaller:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000604 """Installer engine: computes dependencies and installs
605 packages in the right order."""
606
Jack Jansen95839b82003-02-09 23:10:20 +0000607 def __init__(self, db):
608 self._todo = []
609 self._db = db
610 self._curtodo = []
611 self._curmessages = []
612
613 def __contains__(self, package):
614 return package in self._todo
615
616 def _addPackages(self, packages):
617 for package in packages:
618 if not package in self._todo:
619 self._todo.insert(0, package)
620
621 def _prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000622 """Internal routine, recursive engine for prepareInstall.
623
624 Test whether the package is installed and (if not installed
625 or if force==1) prepend it to the temporary todo list and
626 call ourselves recursively on all prerequisites."""
627
Jack Jansen95839b82003-02-09 23:10:20 +0000628 if not force:
629 status, message = package.installed()
630 if status == "yes":
631 return
632 if package in self._todo or package in self._curtodo:
633 return
634 self._curtodo.insert(0, package)
635 if not recursive:
636 return
637 prereqs = package.prerequisites()
638 for pkg, descr in prereqs:
639 if pkg:
640 self._prepareInstall(pkg, force, recursive)
641 else:
642 self._curmessages.append("Requires: %s" % descr)
643
644 def prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000645 """Prepare installation of a package.
646
647 If the package is already installed and force is false nothing
648 is done. If recursive is true prerequisites are installed first.
649
650 Returns a list of packages (to be passed to install) and a list
651 of messages of any problems encountered.
652 """
653
Jack Jansen95839b82003-02-09 23:10:20 +0000654 self._curtodo = []
655 self._curmessages = []
656 self._prepareInstall(package, force, recursive)
657 rv = self._curtodo, self._curmessages
658 self._curtodo = []
659 self._curmessages = []
660 return rv
661
662 def install(self, packages, output):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000663 """Install a list of packages."""
664
Jack Jansen95839b82003-02-09 23:10:20 +0000665 self._addPackages(packages)
666 status = []
667 for pkg in self._todo:
668 msg = pkg.installSinglePackage(output)
669 if msg:
670 status.append(msg)
671 return status
672
673
Jack Jansen95839b82003-02-09 23:10:20 +0000674
675def _run(mode, verbose, force, args):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000676 """Engine for the main program"""
677
Jack Jansen95839b82003-02-09 23:10:20 +0000678 prefs = PimpPreferences()
679 prefs.check()
680 db = PimpDatabase(prefs)
681 db.appendURL(prefs.pimpDatabase)
682
Jack Jansenc4b217d2003-02-10 13:38:44 +0000683 if mode == 'dump':
684 db.dump(sys.stdout)
685 elif mode =='list':
Jack Jansen95839b82003-02-09 23:10:20 +0000686 if not args:
687 args = db.listnames()
688 print "%-20.20s\t%s" % ("Package", "Description")
689 print
690 for pkgname in args:
691 pkg = db.find(pkgname)
692 if pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000693 description = pkg.description()
694 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000695 else:
696 description = 'Error: no such package'
697 print "%-20.20s\t%s" % (pkgname, description)
698 if verbose:
Jack Jansene7b33db2003-02-11 22:40:59 +0000699 print "\tHome page:\t", pkg.homepage()
700 print "\tDownload URL:\t", pkg.downloadURL()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000701 elif mode =='status':
Jack Jansen95839b82003-02-09 23:10:20 +0000702 if not args:
703 args = db.listnames()
704 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
705 print
706 for pkgname in args:
707 pkg = db.find(pkgname)
708 if pkg:
709 status, msg = pkg.installed()
Jack Jansene7b33db2003-02-11 22:40:59 +0000710 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000711 else:
712 status = 'error'
713 msg = 'No such package'
714 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
715 if verbose and status == "no":
716 prereq = pkg.prerequisites()
717 for pkg, msg in prereq:
718 if not pkg:
719 pkg = ''
720 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000721 pkg = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000722 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
723 elif mode == 'install':
724 if not args:
725 print 'Please specify packages to install'
726 sys.exit(1)
727 inst = PimpInstaller(db)
728 for pkgname in args:
729 pkg = db.find(pkgname)
730 if not pkg:
731 print '%s: No such package' % pkgname
732 continue
733 list, messages = inst.prepareInstall(pkg, force)
734 if messages and not force:
735 print "%s: Not installed:" % pkgname
736 for m in messages:
737 print "\t", m
738 else:
739 if verbose:
740 output = sys.stdout
741 else:
742 output = None
743 messages = inst.install(list, output)
744 if messages:
745 print "%s: Not installed:" % pkgname
746 for m in messages:
747 print "\t", m
748
749def main():
Jack Jansen6a600ab2003-02-10 15:55:51 +0000750 """Minimal commandline tool to drive pimp."""
751
Jack Jansen95839b82003-02-09 23:10:20 +0000752 import getopt
753 def _help():
754 print "Usage: pimp [-v] -s [package ...] List installed status"
755 print " pimp [-v] -l [package ...] Show package information"
756 print " pimp [-vf] -i package ... Install packages"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000757 print " pimp -d Dump database to stdout"
Jack Jansen95839b82003-02-09 23:10:20 +0000758 print "Options:"
759 print " -v Verbose"
760 print " -f Force installation"
761 sys.exit(1)
762
763 try:
Jack Jansenc4b217d2003-02-10 13:38:44 +0000764 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
Jack Jansen95839b82003-02-09 23:10:20 +0000765 except getopt.Error:
766 _help()
767 if not opts and not args:
768 _help()
769 mode = None
770 force = 0
771 verbose = 0
772 for o, a in opts:
773 if o == '-s':
774 if mode:
775 _help()
776 mode = 'status'
777 if o == '-l':
778 if mode:
779 _help()
780 mode = 'list'
Jack Jansenc4b217d2003-02-10 13:38:44 +0000781 if o == '-d':
Jack Jansen95839b82003-02-09 23:10:20 +0000782 if mode:
783 _help()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000784 mode = 'dump'
Jack Jansen95839b82003-02-09 23:10:20 +0000785 if o == '-i':
786 mode = 'install'
787 if o == '-f':
788 force = 1
789 if o == '-v':
790 verbose = 1
791 if not mode:
792 _help()
793 _run(mode, verbose, force, args)
794
795if __name__ == '__main__':
796 main()
797
Jack Jansen47e59872003-03-11 14:37:19 +0000798