blob: dbf40db13eb060e0079de9c113942e65706924db [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 Jansena2125fe2003-02-16 23:03:04 +0000360 return [(None,
361 "%s: This package needs to be installed manually (no Download-URL field)" %
362 self.fullname())]
Jack Jansen0dacac42003-02-14 14:11:59 +0000363 if not self._dict.get('Prerequisites'):
Jack Jansen95839b82003-02-09 23:10:20 +0000364 return []
Jack Jansene7b33db2003-02-11 22:40:59 +0000365 for item in self._dict['Prerequisites']:
Jack Jansen95839b82003-02-09 23:10:20 +0000366 if type(item) == str:
367 pkg = None
368 descr = str(item)
369 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000370 name = item['Name']
371 if item.has_key('Version'):
372 name = name + '-' + item['Version']
373 if item.has_key('Flavor'):
374 name = name + '-' + item['Flavor']
375 pkg = self._db.find(name)
Jack Jansen95839b82003-02-09 23:10:20 +0000376 if not pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000377 descr = "Requires unknown %s"%name
Jack Jansen95839b82003-02-09 23:10:20 +0000378 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000379 descr = pkg.description()
Jack Jansen95839b82003-02-09 23:10:20 +0000380 rv.append((pkg, descr))
381 return rv
382
383 def _cmd(self, output, dir, *cmditems):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000384 """Internal routine to run a shell command in a given directory."""
385
Jack Jansen95839b82003-02-09 23:10:20 +0000386 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
387 if output:
388 output.write("+ %s\n" % cmd)
389 if NO_EXECUTE:
390 return 0
Jack Jansen53b341f2003-02-12 15:36:25 +0000391 dummy, fp = os.popen4(cmd, "r")
392 dummy.close()
Jack Jansen95839b82003-02-09 23:10:20 +0000393 while 1:
394 line = fp.readline()
395 if not line:
396 break
397 if output:
398 output.write(line)
399 rv = fp.close()
400 return rv
401
Jack Jansen0dacac42003-02-14 14:11:59 +0000402 def downloadPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000403 """Download a single package, if needed.
404
405 An MD5 signature is used to determine whether download is needed,
406 and to test that we actually downloaded what we expected.
407 If output is given it is a file-like object that will receive a log
408 of what happens.
409
410 If anything unforeseen happened the method returns an error message
411 string.
412 """
413
Jack Jansene7b33db2003-02-11 22:40:59 +0000414 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
Jack Jansen95839b82003-02-09 23:10:20 +0000415 path = urllib.url2pathname(path)
416 filename = os.path.split(path)[1]
Jack Jansenc4b217d2003-02-10 13:38:44 +0000417 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
418 if not self._archiveOK():
Jack Jansen26bf3ac2003-02-10 14:19:14 +0000419 if scheme == 'manual':
420 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansenc4b217d2003-02-10 13:38:44 +0000421 if self._cmd(output, self._db.preferences.downloadDir,
422 "curl",
423 "--output", self.archiveFilename,
Jack Jansene7b33db2003-02-11 22:40:59 +0000424 self._dict['Download-URL']):
Jack Jansenc4b217d2003-02-10 13:38:44 +0000425 return "download command failed"
Jack Jansen95839b82003-02-09 23:10:20 +0000426 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
427 return "archive not found after download"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000428 if not self._archiveOK():
429 return "archive does not have correct MD5 checksum"
430
431 def _archiveOK(self):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000432 """Test an archive. It should exist and the MD5 checksum should be correct."""
433
Jack Jansenc4b217d2003-02-10 13:38:44 +0000434 if not os.path.exists(self.archiveFilename):
435 return 0
Jack Jansene71b9f82003-02-12 16:37:00 +0000436 if not self._dict.get('MD5Sum'):
Jack Jansene7b33db2003-02-11 22:40:59 +0000437 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
Jack Jansenc4b217d2003-02-10 13:38:44 +0000438 return 1
439 data = open(self.archiveFilename, 'rb').read()
440 checksum = md5.new(data).hexdigest()
Jack Jansene7b33db2003-02-11 22:40:59 +0000441 return checksum == self._dict['MD5Sum']
Jack Jansen95839b82003-02-09 23:10:20 +0000442
Jack Jansen0dacac42003-02-14 14:11:59 +0000443 def unpackPackageOnly(self, output=None):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000444 """Unpack a downloaded package archive."""
445
Jack Jansen95839b82003-02-09 23:10:20 +0000446 filename = os.path.split(self.archiveFilename)[1]
447 for ext, cmd in ARCHIVE_FORMATS:
448 if filename[-len(ext):] == ext:
449 break
450 else:
451 return "unknown extension for archive file: %s" % filename
Jack Jansena2125fe2003-02-16 23:03:04 +0000452 self.basename = filename[:-len(ext)]
Jack Jansen95839b82003-02-09 23:10:20 +0000453 cmd = cmd % self.archiveFilename
Jack Jansen95839b82003-02-09 23:10:20 +0000454 if self._cmd(output, self._db.preferences.buildDir, cmd):
455 return "unpack command failed"
Jack Jansen0dacac42003-02-14 14:11:59 +0000456
457 def installPackageOnly(self, output=None):
458 """Default install method, to be overridden by subclasses"""
Jack Jansena2125fe2003-02-16 23:03:04 +0000459 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
460 % (self.fullname(), self._dict.get(flavor, ""))
Jack Jansen95839b82003-02-09 23:10:20 +0000461
Jack Jansen6a600ab2003-02-10 15:55:51 +0000462 def installSinglePackage(self, output=None):
463 """Download, unpack and install a single package.
464
465 If output is given it should be a file-like object and it
466 will receive a log of what happened."""
467
Jack Jansene7b33db2003-02-11 22:40:59 +0000468 if not self._dict['Download-URL']:
Jack Jansena2125fe2003-02-16 23:03:04 +0000469 return "%s: This package needs to be installed manually (no Download-URL field)" % _fmtpackagename(self)
Jack Jansen0dacac42003-02-14 14:11:59 +0000470 msg = self.downloadPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000471 if msg:
Jack Jansena2125fe2003-02-16 23:03:04 +0000472 return "%s: download: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000473
Jack Jansen0dacac42003-02-14 14:11:59 +0000474 msg = self.unpackPackageOnly(output)
Jack Jansen95839b82003-02-09 23:10:20 +0000475 if msg:
Jack Jansena2125fe2003-02-16 23:03:04 +0000476 return "%s: unpack: %s" % (self.fullname(), msg)
Jack Jansen53b341f2003-02-12 15:36:25 +0000477
Jack Jansen0dacac42003-02-14 14:11:59 +0000478 return self.installPackageOnly(output)
Jack Jansen53b341f2003-02-12 15:36:25 +0000479
Jack Jansen0dacac42003-02-14 14:11:59 +0000480 def beforeInstall(self):
481 """Bookkeeping before installation: remember what we have in site-packages"""
482 self._old_contents = os.listdir(self._db.preferences.installDir)
483
484 def afterInstall(self):
485 """Bookkeeping after installation: interpret any new .pth files that have
486 appeared"""
487
Jack Jansen53b341f2003-02-12 15:36:25 +0000488 new_contents = os.listdir(self._db.preferences.installDir)
Jack Jansen53b341f2003-02-12 15:36:25 +0000489 for fn in new_contents:
Jack Jansen0dacac42003-02-14 14:11:59 +0000490 if fn in self._old_contents:
Jack Jansen53b341f2003-02-12 15:36:25 +0000491 continue
492 if fn[-4:] != '.pth':
493 continue
494 fullname = os.path.join(self._db.preferences.installDir, fn)
495 f = open(fullname)
496 for line in f.readlines():
497 if not line:
498 continue
499 if line[0] == '#':
500 continue
501 if line[:6] == 'import':
502 exec line
503 continue
504 if line[-1] == '\n':
505 line = line[:-1]
506 if not os.path.isabs(line):
507 line = os.path.join(self._db.preferences.installDir, line)
508 line = os.path.realpath(line)
509 if not line in sys.path:
Jack Jansen0dacac42003-02-14 14:11:59 +0000510 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000511
Jack Jansen0dacac42003-02-14 14:11:59 +0000512class PimpPackage_binary(PimpPackage):
513
514 def unpackPackageOnly(self, output=None):
515 """We don't unpack binary packages until installing"""
516 pass
517
518 def installPackageOnly(self, output=None):
519 """Install a single source package.
520
521 If output is given it should be a file-like object and it
522 will receive a log of what happened."""
Jack Jansena2125fe2003-02-16 23:03:04 +0000523 print 'PimpPackage_binary installPackageOnly'
Jack Jansen0dacac42003-02-14 14:11:59 +0000524
525 msgs = []
526 if self._dict.has_key('Pre-install-command'):
527 msg.append("%s: Pre-install-command ignored" % self.fullname())
528 if self._dict.has_key('Install-command'):
529 msgs.append("%s: Install-command ignored" % self.fullname())
530 if self._dict.has_key('Post-install-command'):
531 msgs.append("%s: Post-install-command ignored" % self.fullname())
532
533 self.beforeInstall()
534
535 # Install by unpacking
536 filename = os.path.split(self.archiveFilename)[1]
537 for ext, cmd in ARCHIVE_FORMATS:
538 if filename[-len(ext):] == ext:
539 break
540 else:
541 return "unknown extension for archive file: %s" % filename
542
543 # Modify where the files are extracted
544 prefixmod = '-C /'
545 cmd = cmd % self.archiveFilename
546 if self._cmd(output, self._db.preferences.buildDir, cmd, prefixmod):
547 return "unpack command failed"
548
549 self.afterInstall()
550
551 if self._dict.has_key('Post-install-command'):
552 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
553 return "post-install %s: running \"%s\" failed" % \
554 (self.fullname(), self._dict['Post-install-command'])
555 return None
556
557
558class PimpPackage_source(PimpPackage):
559
560 def unpackPackageOnly(self, output=None):
561 """Unpack a source package and check that setup.py exists"""
562 PimpPackage.unpackPackageOnly(self, output)
563 # Test that a setup script has been create
Jack Jansena2125fe2003-02-16 23:03:04 +0000564 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
Jack Jansen0dacac42003-02-14 14:11:59 +0000565 setupname = os.path.join(self._buildDirname, "setup.py")
566 if not os.path.exists(setupname) and not NO_EXECUTE:
567 return "no setup.py found after unpack of archive"
568
569 def installPackageOnly(self, output=None):
570 """Install a single source package.
571
572 If output is given it should be a file-like object and it
573 will receive a log of what happened."""
574
575 if self._dict.has_key('Pre-install-command'):
576 if self._cmd(output, self._buildDirname, self._dict['Pre-install-command']):
577 return "pre-install %s: running \"%s\" failed" % \
578 (self.fullname(), self._dict['Pre-install-command'])
579
580 self.beforeInstall()
581 installcmd = self._dict.get('Install-command')
582 if not installcmd:
583 installcmd = '"%s" setup.py install' % sys.executable
584 if self._cmd(output, self._buildDirname, installcmd):
585 return "install %s: running \"%s\" failed" % self.fullname()
586
587 self.afterInstall()
588
589 if self._dict.has_key('Post-install-command'):
590 if self._cmd(output, self._buildDirname, self._dict['Post-install-command']):
591 return "post-install %s: running \"%s\" failed" % \
592 (self.fullname(), self._dict['Post-install-command'])
593 return None
594
595
Jack Jansen95839b82003-02-09 23:10:20 +0000596class PimpInstaller:
Jack Jansen6a600ab2003-02-10 15:55:51 +0000597 """Installer engine: computes dependencies and installs
598 packages in the right order."""
599
Jack Jansen95839b82003-02-09 23:10:20 +0000600 def __init__(self, db):
601 self._todo = []
602 self._db = db
603 self._curtodo = []
604 self._curmessages = []
605
606 def __contains__(self, package):
607 return package in self._todo
608
609 def _addPackages(self, packages):
610 for package in packages:
611 if not package in self._todo:
612 self._todo.insert(0, package)
613
614 def _prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000615 """Internal routine, recursive engine for prepareInstall.
616
617 Test whether the package is installed and (if not installed
618 or if force==1) prepend it to the temporary todo list and
619 call ourselves recursively on all prerequisites."""
620
Jack Jansen95839b82003-02-09 23:10:20 +0000621 if not force:
622 status, message = package.installed()
623 if status == "yes":
624 return
625 if package in self._todo or package in self._curtodo:
626 return
627 self._curtodo.insert(0, package)
628 if not recursive:
629 return
630 prereqs = package.prerequisites()
631 for pkg, descr in prereqs:
632 if pkg:
633 self._prepareInstall(pkg, force, recursive)
634 else:
635 self._curmessages.append("Requires: %s" % descr)
636
637 def prepareInstall(self, package, force=0, recursive=1):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000638 """Prepare installation of a package.
639
640 If the package is already installed and force is false nothing
641 is done. If recursive is true prerequisites are installed first.
642
643 Returns a list of packages (to be passed to install) and a list
644 of messages of any problems encountered.
645 """
646
Jack Jansen95839b82003-02-09 23:10:20 +0000647 self._curtodo = []
648 self._curmessages = []
649 self._prepareInstall(package, force, recursive)
650 rv = self._curtodo, self._curmessages
651 self._curtodo = []
652 self._curmessages = []
653 return rv
654
655 def install(self, packages, output):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000656 """Install a list of packages."""
657
Jack Jansen95839b82003-02-09 23:10:20 +0000658 self._addPackages(packages)
659 status = []
660 for pkg in self._todo:
661 msg = pkg.installSinglePackage(output)
662 if msg:
663 status.append(msg)
664 return status
665
666
Jack Jansen95839b82003-02-09 23:10:20 +0000667
668def _run(mode, verbose, force, args):
Jack Jansen6a600ab2003-02-10 15:55:51 +0000669 """Engine for the main program"""
670
Jack Jansen95839b82003-02-09 23:10:20 +0000671 prefs = PimpPreferences()
672 prefs.check()
673 db = PimpDatabase(prefs)
674 db.appendURL(prefs.pimpDatabase)
675
Jack Jansenc4b217d2003-02-10 13:38:44 +0000676 if mode == 'dump':
677 db.dump(sys.stdout)
678 elif mode =='list':
Jack Jansen95839b82003-02-09 23:10:20 +0000679 if not args:
680 args = db.listnames()
681 print "%-20.20s\t%s" % ("Package", "Description")
682 print
683 for pkgname in args:
684 pkg = db.find(pkgname)
685 if pkg:
Jack Jansene7b33db2003-02-11 22:40:59 +0000686 description = pkg.description()
687 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000688 else:
689 description = 'Error: no such package'
690 print "%-20.20s\t%s" % (pkgname, description)
691 if verbose:
Jack Jansene7b33db2003-02-11 22:40:59 +0000692 print "\tHome page:\t", pkg.homepage()
693 print "\tDownload URL:\t", pkg.downloadURL()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000694 elif mode =='status':
Jack Jansen95839b82003-02-09 23:10:20 +0000695 if not args:
696 args = db.listnames()
697 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
698 print
699 for pkgname in args:
700 pkg = db.find(pkgname)
701 if pkg:
702 status, msg = pkg.installed()
Jack Jansene7b33db2003-02-11 22:40:59 +0000703 pkgname = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000704 else:
705 status = 'error'
706 msg = 'No such package'
707 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
708 if verbose and status == "no":
709 prereq = pkg.prerequisites()
710 for pkg, msg in prereq:
711 if not pkg:
712 pkg = ''
713 else:
Jack Jansene7b33db2003-02-11 22:40:59 +0000714 pkg = pkg.fullname()
Jack Jansen95839b82003-02-09 23:10:20 +0000715 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
716 elif mode == 'install':
717 if not args:
718 print 'Please specify packages to install'
719 sys.exit(1)
720 inst = PimpInstaller(db)
721 for pkgname in args:
722 pkg = db.find(pkgname)
723 if not pkg:
724 print '%s: No such package' % pkgname
725 continue
726 list, messages = inst.prepareInstall(pkg, force)
727 if messages and not force:
728 print "%s: Not installed:" % pkgname
729 for m in messages:
730 print "\t", m
731 else:
732 if verbose:
733 output = sys.stdout
734 else:
735 output = None
736 messages = inst.install(list, output)
737 if messages:
738 print "%s: Not installed:" % pkgname
739 for m in messages:
740 print "\t", m
741
742def main():
Jack Jansen6a600ab2003-02-10 15:55:51 +0000743 """Minimal commandline tool to drive pimp."""
744
Jack Jansen95839b82003-02-09 23:10:20 +0000745 import getopt
746 def _help():
747 print "Usage: pimp [-v] -s [package ...] List installed status"
748 print " pimp [-v] -l [package ...] Show package information"
749 print " pimp [-vf] -i package ... Install packages"
Jack Jansenc4b217d2003-02-10 13:38:44 +0000750 print " pimp -d Dump database to stdout"
Jack Jansen95839b82003-02-09 23:10:20 +0000751 print "Options:"
752 print " -v Verbose"
753 print " -f Force installation"
754 sys.exit(1)
755
756 try:
Jack Jansenc4b217d2003-02-10 13:38:44 +0000757 opts, args = getopt.getopt(sys.argv[1:], "slifvd")
Jack Jansen95839b82003-02-09 23:10:20 +0000758 except getopt.Error:
759 _help()
760 if not opts and not args:
761 _help()
762 mode = None
763 force = 0
764 verbose = 0
765 for o, a in opts:
766 if o == '-s':
767 if mode:
768 _help()
769 mode = 'status'
770 if o == '-l':
771 if mode:
772 _help()
773 mode = 'list'
Jack Jansenc4b217d2003-02-10 13:38:44 +0000774 if o == '-d':
Jack Jansen95839b82003-02-09 23:10:20 +0000775 if mode:
776 _help()
Jack Jansenc4b217d2003-02-10 13:38:44 +0000777 mode = 'dump'
Jack Jansen95839b82003-02-09 23:10:20 +0000778 if o == '-i':
779 mode = 'install'
780 if o == '-f':
781 force = 1
782 if o == '-v':
783 verbose = 1
784 if not mode:
785 _help()
786 _run(mode, verbose, force, args)
787
788if __name__ == '__main__':
789 main()
790
791