blob: bff5f6c5157c004d1bf9ad16c7f446ca29900cb5 [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.
Jack Jansenc5991b02003-06-29 00:09:18 +00004Despite other rumours the name stands for "Packman IMPlementation".
Jack Jansen6a600ab2003-02-10 15:55:51 +00005
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 Jansen6fde1ce2003-04-15 14:43:05 +000025import tarfile
26import tempfile
27import shutil
Jack Jansen989ddc02004-03-11 23:03:59 +000028import time
Jack Jansen95839b82003-02-09 23:10:20 +000029
Jack Jansenb789a062003-05-28 18:56:30 +000030__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main",
31 "PIMP_VERSION", "main"]
Jack Jansen6a600ab2003-02-10 15:55:51 +000032
Jack Jansen95839b82003-02-09 23:10:20 +000033_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
34_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
35_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
36
37NO_EXECUTE=0
38
Jack Jansenafd63b92004-02-28 22:34:02 +000039PIMP_VERSION="0.4"
Jack Jansene7b33db2003-02-11 22:40:59 +000040
Jack Jansen0dacac42003-02-14 14:11:59 +000041# Flavors:
42# source: setup-based package
43# binary: tar (or other) archive created with setup.py bdist.
Jack Jansen95839b82003-02-09 23:10:20 +000044DEFAULT_FLAVORORDER=['source', 'binary']
45DEFAULT_DOWNLOADDIR='/tmp'
46DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000047DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansenafd63b92004-02-28 22:34:02 +000048DEFAULT_PIMPDATABASE_FMT="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist"
Jack Jansen95839b82003-02-09 23:10:20 +000049
Jack Jansen192bd962004-02-28 23:18:43 +000050def getDefaultDatabase(experimental=False):
Jack Jansen989ddc02004-03-11 23:03:59 +000051 if experimental:
52 status = "exp"
53 else:
54 status = "prod"
55
56 major, minor, micro, state, extra = sys.version_info
57 pyvers = '%d.%d' % (major, minor)
58 if state != 'final':
59 pyvers = pyvers + '%s%d' % (state, extra)
60
61 longplatform = distutils.util.get_platform()
62 osname, release, machine = longplatform.split('-')
63 # For some platforms we may want to differentiate between
64 # installation types
65 if osname == 'darwin':
66 if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'):
67 osname = 'darwin_apple'
68 elif sys.prefix.startswith('/Library/Frameworks/Python.framework'):
69 osname = 'darwin_macpython'
70 # Otherwise we don't know...
71 # Now we try various URLs by playing with the release string.
72 # We remove numbers off the end until we find a match.
73 rel = release
74 while True:
75 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine)
76 try:
77 urllib2.urlopen(url)
78 except urllib2.HTTPError, arg:
79 pass
80 else:
81 break
82 if not rel:
83 # We're out of version numbers to try. Use the
84 # full release number, this will give a reasonable
85 # error message later
86 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine)
87 break
88 idx = rel.rfind('.')
89 if idx < 0:
90 rel = ''
91 else:
92 rel = rel[:idx]
93 return url
Jack Jansen192bd962004-02-28 23:18:43 +000094
Jack Jansen6fde1ce2003-04-15 14:43:05 +000095def _cmd(output, dir, *cmditems):
96 """Internal routine to run a shell command in a given directory."""
97
98 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
99 if output:
100 output.write("+ %s\n" % cmd)
101 if NO_EXECUTE:
102 return 0
103 child = popen2.Popen4(cmd)
104 child.tochild.close()
105 while 1:
106 line = child.fromchild.readline()
107 if not line:
108 break
109 if output:
110 output.write(line)
111 return child.wait()
112
Jack Jansen989ddc02004-03-11 23:03:59 +0000113class PimpDownloader:
114 """Abstract base class - Downloader for archives"""
115
116 def __init__(self, argument,
117 dir="",
118 watcher=None):
119 self.argument = argument
120 self._dir = dir
121 self._watcher = watcher
122
123 def download(self, url, filename, output=None):
124 return None
125
126 def update(self, str):
127 if self._watcher:
128 return self._watcher.update(str)
129 return True
130
131class PimpCurlDownloader(PimpDownloader):
132
133 def download(self, url, filename, output=None):
134 self.update("Downloading %s..." % url)
135 exitstatus = _cmd(output, self._dir,
136 "curl",
137 "--output", filename,
138 url)
139 self.update("Downloading %s: finished" % url)
140 return (not exitstatus)
141
142class PimpUrllibDownloader(PimpDownloader):
143
144 def download(self, url, filename, output=None):
145 output = open(filename, 'wb')
146 self.update("Downloading %s: opening connection" % url)
147 keepgoing = True
148 download = urllib2.urlopen(url)
149 if download.headers.has_key("content-length"):
150 length = long(download.headers['content-length'])
151 else:
152 length = -1
153
154 data = download.read(4096) #read 4K at a time
155 dlsize = 0
156 lasttime = 0
157 while keepgoing:
158 dlsize = dlsize + len(data)
159 if len(data) == 0:
160 #this is our exit condition
161 break
162 output.write(data)
163 if int(time.time()) != lasttime:
164 # Update at most once per second
165 lasttime = int(time.time())
166 if length == -1:
167 keepgoing = self.update("Downloading %s: %d bytes..." % (url, dlsize))
168 else:
169 keepgoing = self.update("Downloading %s: %d%% (%d bytes)..." % (url, int(100.0*dlsize/length), dlsize))
170 data = download.read(4096)
171 if keepgoing:
172 self.update("Downloading %s: finished" % url)
173 return keepgoing
174
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000175class PimpUnpacker:
176 """Abstract base class - Unpacker for archives"""
177
178 _can_rename = False
179
180 def __init__(self, argument,
181 dir="",
Jack Jansen989ddc02004-03-11 23:03:59 +0000182 renames=[],
183 watcher=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000184 self.argument = argument
185 if renames and not self._can_rename:
186 raise RuntimeError, "This unpacker cannot rename files"
187 self._dir = dir
188 self._renames = renames
Jack Jansen989ddc02004-03-11 23:03:59 +0000189 self._watcher = watcher
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000190
Jack Jansen5da131b2003-06-01 20:57:12 +0000191 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000192 return None
193
Jack Jansen989ddc02004-03-11 23:03:59 +0000194 def update(self, str):
195 if self._watcher:
196 return self._watcher.update(str)
197 return True
198
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000199class PimpCommandUnpacker(PimpUnpacker):
200 """Unpack archives by calling a Unix utility"""
201
202 _can_rename = False
203
Jack Jansen5da131b2003-06-01 20:57:12 +0000204 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000205 cmd = self.argument % archive
206 if _cmd(output, self._dir, cmd):
207 return "unpack command failed"
208
209class PimpTarUnpacker(PimpUnpacker):
210 """Unpack tarfiles using the builtin tarfile module"""
211
212 _can_rename = True
213
Jack Jansen5da131b2003-06-01 20:57:12 +0000214 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000215 tf = tarfile.open(archive, "r")
216 members = tf.getmembers()
217 skip = []
218 if self._renames:
219 for member in members:
220 for oldprefix, newprefix in self._renames:
221 if oldprefix[:len(self._dir)] == self._dir:
222 oldprefix2 = oldprefix[len(self._dir):]
223 else:
224 oldprefix2 = None
225 if member.name[:len(oldprefix)] == oldprefix:
226 if newprefix is None:
227 skip.append(member)
228 #print 'SKIP', member.name
229 else:
230 member.name = newprefix + member.name[len(oldprefix):]
231 print ' ', member.name
232 break
233 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
234 if newprefix is None:
235 skip.append(member)
236 #print 'SKIP', member.name
237 else:
238 member.name = newprefix + member.name[len(oldprefix2):]
239 #print ' ', member.name
240 break
241 else:
242 skip.append(member)
243 #print '????', member.name
244 for member in members:
245 if member in skip:
Jack Jansen989ddc02004-03-11 23:03:59 +0000246 self.update("Skipping %s" % member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000247 continue
Jack Jansen989ddc02004-03-11 23:03:59 +0000248 self.update("Extracting %s" % member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000249 tf.extract(member, self._dir)
250 if skip:
251 names = [member.name for member in skip if member.name[-1] != '/']
Jack Jansen5da131b2003-06-01 20:57:12 +0000252 if package:
253 names = package.filterExpectedSkips(names)
Jack Jansen6432f782003-04-22 13:56:19 +0000254 if names:
Jack Jansen705553a2003-05-06 12:44:00 +0000255 return "Not all files were unpacked: %s" % " ".join(names)
Jack Jansen5da131b2003-06-01 20:57:12 +0000256
Jack Jansen95839b82003-02-09 23:10:20 +0000257ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000258 (".tar.Z", PimpTarUnpacker, None),
259 (".taz", PimpTarUnpacker, None),
260 (".tar.gz", PimpTarUnpacker, None),
261 (".tgz", PimpTarUnpacker, None),
262 (".tar.bz", PimpTarUnpacker, None),
263 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000264]
265
266class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000267 """Container for per-user preferences, such as the database to use
268 and where to install packages."""
269
270 def __init__(self,
271 flavorOrder=None,
272 downloadDir=None,
273 buildDir=None,
274 installDir=None,
275 pimpDatabase=None):
276 if not flavorOrder:
277 flavorOrder = DEFAULT_FLAVORORDER
278 if not downloadDir:
279 downloadDir = DEFAULT_DOWNLOADDIR
280 if not buildDir:
281 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000282 if not pimpDatabase:
Jack Jansen192bd962004-02-28 23:18:43 +0000283 pimpDatabase = getDefaultDatabase()
Jack Jansen20fa6752003-04-16 12:15:34 +0000284 self.setInstallDir(installDir)
285 self.flavorOrder = flavorOrder
286 self.downloadDir = downloadDir
287 self.buildDir = buildDir
288 self.pimpDatabase = pimpDatabase
Jack Jansen989ddc02004-03-11 23:03:59 +0000289 self.watcher = None
290
291 def setWatcher(self, watcher):
292 self.watcher = watcher
Jack Jansen20fa6752003-04-16 12:15:34 +0000293
294 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000295 if installDir:
296 # Installing to non-standard location.
297 self.installLocations = [
298 ('--install-lib', installDir),
299 ('--install-headers', None),
300 ('--install-scripts', None),
301 ('--install-data', None)]
302 else:
303 installDir = DEFAULT_INSTALLDIR
304 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000305 self.installDir = installDir
Jack Jansen5da131b2003-06-01 20:57:12 +0000306
307 def isUserInstall(self):
308 return self.installDir != DEFAULT_INSTALLDIR
Jack Jansen20fa6752003-04-16 12:15:34 +0000309
Jack Jansen0ae32202003-04-09 13:25:43 +0000310 def check(self):
311 """Check that the preferences make sense: directories exist and are
312 writable, the install directory is on sys.path, etc."""
313
314 rv = ""
315 RWX_OK = os.R_OK|os.W_OK|os.X_OK
316 if not os.path.exists(self.downloadDir):
317 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
318 elif not os.access(self.downloadDir, RWX_OK):
319 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
320 if not os.path.exists(self.buildDir):
321 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
322 elif not os.access(self.buildDir, RWX_OK):
323 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
324 if not os.path.exists(self.installDir):
325 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
326 elif not os.access(self.installDir, RWX_OK):
327 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
328 else:
329 installDir = os.path.realpath(self.installDir)
330 for p in sys.path:
331 try:
332 realpath = os.path.realpath(p)
333 except:
334 pass
335 if installDir == realpath:
336 break
337 else:
338 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000339 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000340
341 def compareFlavors(self, left, right):
342 """Compare two flavor strings. This is part of your preferences
343 because whether the user prefers installing from source or binary is."""
344 if left in self.flavorOrder:
345 if right in self.flavorOrder:
346 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
347 return -1
348 if right in self.flavorOrder:
349 return 1
350 return cmp(left, right)
351
Jack Jansen95839b82003-02-09 23:10:20 +0000352class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000353 """Class representing a pimp database. It can actually contain
354 information from multiple databases through inclusion, but the
355 toplevel database is considered the master, as its maintainer is
356 "responsible" for the contents."""
357
358 def __init__(self, prefs):
359 self._packages = []
360 self.preferences = prefs
361 self._urllist = []
362 self._version = ""
363 self._maintainer = ""
364 self._description = ""
365
366 def close(self):
367 """Clean up"""
368 self._packages = []
369 self.preferences = None
370
371 def appendURL(self, url, included=0):
372 """Append packages from the database with the given URL.
373 Only the first database should specify included=0, so the
374 global information (maintainer, description) get stored."""
375
376 if url in self._urllist:
377 return
378 self._urllist.append(url)
379 fp = urllib2.urlopen(url).fp
380 dict = plistlib.Plist.fromFile(fp)
381 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000382 if included:
383 version = dict.get('Version')
384 if version and version > self._version:
385 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
386 (url, version))
387 else:
388 self._version = dict.get('Version')
389 if not self._version:
390 sys.stderr.write("Warning: database has no Version information\n")
391 elif self._version > PIMP_VERSION:
392 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000393 % (self._version, PIMP_VERSION))
394 self._maintainer = dict.get('Maintainer', '')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000395 self._description = dict.get('Description', '').strip()
Jack Jansen0ae32202003-04-09 13:25:43 +0000396 self._appendPackages(dict['Packages'])
397 others = dict.get('Include', [])
398 for url in others:
399 self.appendURL(url, included=1)
400
401 def _appendPackages(self, packages):
402 """Given a list of dictionaries containing package
403 descriptions create the PimpPackage objects and append them
404 to our internal storage."""
405
406 for p in packages:
407 p = dict(p)
408 flavor = p.get('Flavor')
409 if flavor == 'source':
410 pkg = PimpPackage_source(self, p)
411 elif flavor == 'binary':
412 pkg = PimpPackage_binary(self, p)
413 else:
414 pkg = PimpPackage(self, dict(p))
415 self._packages.append(pkg)
416
417 def list(self):
418 """Return a list of all PimpPackage objects in the database."""
419
420 return self._packages
421
422 def listnames(self):
423 """Return a list of names of all packages in the database."""
424
425 rv = []
426 for pkg in self._packages:
427 rv.append(pkg.fullname())
428 rv.sort()
429 return rv
430
431 def dump(self, pathOrFile):
432 """Dump the contents of the database to an XML .plist file.
433
434 The file can be passed as either a file object or a pathname.
435 All data, including included databases, is dumped."""
436
437 packages = []
438 for pkg in self._packages:
439 packages.append(pkg.dump())
440 dict = {
441 'Version': self._version,
442 'Maintainer': self._maintainer,
443 'Description': self._description,
444 'Packages': packages
445 }
446 plist = plistlib.Plist(**dict)
447 plist.write(pathOrFile)
448
449 def find(self, ident):
450 """Find a package. The package can be specified by name
451 or as a dictionary with name, version and flavor entries.
452
453 Only name is obligatory. If there are multiple matches the
454 best one (higher version number, flavors ordered according to
455 users' preference) is returned."""
456
457 if type(ident) == str:
458 # Remove ( and ) for pseudo-packages
459 if ident[0] == '(' and ident[-1] == ')':
460 ident = ident[1:-1]
461 # Split into name-version-flavor
462 fields = ident.split('-')
463 if len(fields) < 1 or len(fields) > 3:
464 return None
465 name = fields[0]
466 if len(fields) > 1:
467 version = fields[1]
468 else:
469 version = None
470 if len(fields) > 2:
471 flavor = fields[2]
472 else:
473 flavor = None
474 else:
475 name = ident['Name']
476 version = ident.get('Version')
477 flavor = ident.get('Flavor')
478 found = None
479 for p in self._packages:
480 if name == p.name() and \
481 (not version or version == p.version()) and \
482 (not flavor or flavor == p.flavor()):
483 if not found or found < p:
484 found = p
485 return found
486
Jack Jansene7b33db2003-02-11 22:40:59 +0000487ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000488 "Name",
489 "Version",
490 "Flavor",
491 "Description",
492 "Home-page",
493 "Download-URL",
494 "Install-test",
495 "Install-command",
496 "Pre-install-command",
497 "Post-install-command",
498 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000499 "MD5Sum",
500 "User-install-skips",
501 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000502]
503
Jack Jansen95839b82003-02-09 23:10:20 +0000504class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000505 """Class representing a single package."""
506
507 def __init__(self, db, dict):
508 self._db = db
509 name = dict["Name"]
510 for k in dict.keys():
511 if not k in ALLOWED_KEYS:
512 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
513 self._dict = dict
514
515 def __getitem__(self, key):
516 return self._dict[key]
517
518 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000519 def version(self): return self._dict.get('Version')
520 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000521 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000522 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000523 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000524 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000525 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Jack Jansen0ae32202003-04-09 13:25:43 +0000526
527 def fullname(self):
528 """Return the full name "name-version-flavor" of a package.
529
530 If the package is a pseudo-package, something that cannot be
531 installed through pimp, return the name in (parentheses)."""
532
533 rv = self._dict['Name']
534 if self._dict.has_key('Version'):
535 rv = rv + '-%s' % self._dict['Version']
536 if self._dict.has_key('Flavor'):
537 rv = rv + '-%s' % self._dict['Flavor']
538 if not self._dict.get('Download-URL'):
539 # Pseudo-package, show in parentheses
540 rv = '(%s)' % rv
541 return rv
542
543 def dump(self):
544 """Return a dict object containing the information on the package."""
545 return self._dict
546
547 def __cmp__(self, other):
548 """Compare two packages, where the "better" package sorts lower."""
549
550 if not isinstance(other, PimpPackage):
551 return cmp(id(self), id(other))
552 if self.name() != other.name():
553 return cmp(self.name(), other.name())
554 if self.version() != other.version():
555 return -cmp(self.version(), other.version())
556 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
557
558 def installed(self):
559 """Test wheter the package is installed.
560
561 Returns two values: a status indicator which is one of
562 "yes", "no", "old" (an older version is installed) or "bad"
563 (something went wrong during the install test) and a human
564 readable string which may contain more details."""
565
566 namespace = {
567 "NotInstalled": _scriptExc_NotInstalled,
568 "OldInstalled": _scriptExc_OldInstalled,
569 "BadInstalled": _scriptExc_BadInstalled,
570 "os": os,
571 "sys": sys,
572 }
573 installTest = self._dict['Install-test'].strip() + '\n'
574 try:
575 exec installTest in namespace
576 except ImportError, arg:
577 return "no", str(arg)
578 except _scriptExc_NotInstalled, arg:
579 return "no", str(arg)
580 except _scriptExc_OldInstalled, arg:
581 return "old", str(arg)
582 except _scriptExc_BadInstalled, arg:
583 return "bad", str(arg)
584 except:
585 sys.stderr.write("-------------------------------------\n")
586 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
587 sys.stderr.write("---- source:\n")
588 sys.stderr.write(installTest)
589 sys.stderr.write("---- exception:\n")
590 import traceback
591 traceback.print_exc(file=sys.stderr)
592 if self._db._maintainer:
593 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
594 sys.stderr.write("-------------------------------------\n")
595 return "bad", "Package install test got exception"
596 return "yes", ""
597
598 def prerequisites(self):
599 """Return a list of prerequisites for this package.
600
601 The list contains 2-tuples, of which the first item is either
602 a PimpPackage object or None, and the second is a descriptive
603 string. The first item can be None if this package depends on
604 something that isn't pimp-installable, in which case the descriptive
605 string should tell the user what to do."""
606
607 rv = []
608 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000609 # For pseudo-packages that are already installed we don't
610 # return an error message
611 status, _ = self.installed()
612 if status == "yes":
613 return []
Jack Jansen0ae32202003-04-09 13:25:43 +0000614 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000615 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000616 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000617 if self.systemwideOnly() and self._db.preferences.isUserInstall():
618 return [(None,
619 "%s: This package can only be installed system-wide" %
620 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000621 if not self._dict.get('Prerequisites'):
622 return []
623 for item in self._dict['Prerequisites']:
624 if type(item) == str:
625 pkg = None
626 descr = str(item)
627 else:
628 name = item['Name']
629 if item.has_key('Version'):
630 name = name + '-' + item['Version']
631 if item.has_key('Flavor'):
632 name = name + '-' + item['Flavor']
633 pkg = self._db.find(name)
634 if not pkg:
635 descr = "Requires unknown %s"%name
636 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000637 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000638 rv.append((pkg, descr))
639 return rv
640
Jack Jansen0ae32202003-04-09 13:25:43 +0000641
642 def downloadPackageOnly(self, output=None):
643 """Download a single package, if needed.
644
645 An MD5 signature is used to determine whether download is needed,
646 and to test that we actually downloaded what we expected.
647 If output is given it is a file-like object that will receive a log
648 of what happens.
649
650 If anything unforeseen happened the method returns an error message
651 string.
652 """
653
654 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
655 path = urllib.url2pathname(path)
656 filename = os.path.split(path)[1]
657 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
658 if not self._archiveOK():
659 if scheme == 'manual':
660 return "Please download package manually and save as %s" % self.archiveFilename
Jack Jansen989ddc02004-03-11 23:03:59 +0000661 downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir,
662 watcher=self._db.preferences.watcher)
663 if not downloader.download(self._dict['Download-URL'],
664 self.archiveFilename, output):
Jack Jansen0ae32202003-04-09 13:25:43 +0000665 return "download command failed"
666 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
667 return "archive not found after download"
668 if not self._archiveOK():
669 return "archive does not have correct MD5 checksum"
670
671 def _archiveOK(self):
672 """Test an archive. It should exist and the MD5 checksum should be correct."""
673
674 if not os.path.exists(self.archiveFilename):
675 return 0
676 if not self._dict.get('MD5Sum'):
677 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
678 return 1
679 data = open(self.archiveFilename, 'rb').read()
680 checksum = md5.new(data).hexdigest()
681 return checksum == self._dict['MD5Sum']
682
683 def unpackPackageOnly(self, output=None):
684 """Unpack a downloaded package archive."""
685
686 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000687 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000688 if filename[-len(ext):] == ext:
689 break
690 else:
691 return "unknown extension for archive file: %s" % filename
692 self.basename = filename[:-len(ext)]
Jack Jansen989ddc02004-03-11 23:03:59 +0000693 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir,
694 watcher=self._db.preferences.watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000695 rv = unpacker.unpack(self.archiveFilename, output=output)
696 if rv:
697 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000698
699 def installPackageOnly(self, output=None):
700 """Default install method, to be overridden by subclasses"""
701 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
702 % (self.fullname(), self._dict.get(flavor, ""))
703
704 def installSinglePackage(self, output=None):
705 """Download, unpack and install a single package.
706
707 If output is given it should be a file-like object and it
708 will receive a log of what happened."""
709
Jack Jansen749f4812003-07-21 20:47:11 +0000710 if not self._dict.get('Download-URL'):
711 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000712 msg = self.downloadPackageOnly(output)
713 if msg:
714 return "%s: download: %s" % (self.fullname(), msg)
715
716 msg = self.unpackPackageOnly(output)
717 if msg:
718 return "%s: unpack: %s" % (self.fullname(), msg)
719
720 return self.installPackageOnly(output)
721
722 def beforeInstall(self):
723 """Bookkeeping before installation: remember what we have in site-packages"""
724 self._old_contents = os.listdir(self._db.preferences.installDir)
725
726 def afterInstall(self):
727 """Bookkeeping after installation: interpret any new .pth files that have
728 appeared"""
729
730 new_contents = os.listdir(self._db.preferences.installDir)
731 for fn in new_contents:
732 if fn in self._old_contents:
733 continue
734 if fn[-4:] != '.pth':
735 continue
736 fullname = os.path.join(self._db.preferences.installDir, fn)
737 f = open(fullname)
738 for line in f.readlines():
739 if not line:
740 continue
741 if line[0] == '#':
742 continue
743 if line[:6] == 'import':
744 exec line
745 continue
746 if line[-1] == '\n':
747 line = line[:-1]
748 if not os.path.isabs(line):
749 line = os.path.join(self._db.preferences.installDir, line)
750 line = os.path.realpath(line)
751 if not line in sys.path:
752 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000753
Jack Jansen5da131b2003-06-01 20:57:12 +0000754 def filterExpectedSkips(self, names):
755 """Return a list that contains only unpexpected skips"""
756 if not self._db.preferences.isUserInstall():
757 return names
758 expected_skips = self._dict.get('User-install-skips')
759 if not expected_skips:
760 return names
761 newnames = []
762 for name in names:
763 for skip in expected_skips:
764 if name[:len(skip)] == skip:
765 break
766 else:
767 newnames.append(name)
768 return newnames
769
Jack Jansen0dacac42003-02-14 14:11:59 +0000770class PimpPackage_binary(PimpPackage):
771
Jack Jansen0ae32202003-04-09 13:25:43 +0000772 def unpackPackageOnly(self, output=None):
773 """We don't unpack binary packages until installing"""
774 pass
775
776 def installPackageOnly(self, output=None):
777 """Install a single source package.
778
779 If output is given it should be a file-like object and it
780 will receive a log of what happened."""
Jack Jansen0ae32202003-04-09 13:25:43 +0000781
Jack Jansen0ae32202003-04-09 13:25:43 +0000782 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000783 return "%s: Binary package cannot have Install-command" % self.fullname()
784
785 if self._dict.has_key('Pre-install-command'):
786 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
787 return "pre-install %s: running \"%s\" failed" % \
788 (self.fullname(), self._dict['Pre-install-command'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000789
790 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000791
Jack Jansen0ae32202003-04-09 13:25:43 +0000792 # Install by unpacking
793 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000794 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000795 if filename[-len(ext):] == ext:
796 break
797 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000798 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
799 self.basename = filename[:-len(ext)]
Jack Jansen0ae32202003-04-09 13:25:43 +0000800
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000801 install_renames = []
802 for k, newloc in self._db.preferences.installLocations:
803 if not newloc:
804 continue
805 if k == "--install-lib":
806 oldloc = DEFAULT_INSTALLDIR
807 else:
808 return "%s: Don't know installLocation %s" % (self.fullname(), k)
809 install_renames.append((oldloc, newloc))
810
811 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000812 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000813 if rv:
814 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000815
816 self.afterInstall()
817
818 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000819 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
820 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000821 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000822
Jack Jansen0ae32202003-04-09 13:25:43 +0000823 return None
824
825
Jack Jansen0dacac42003-02-14 14:11:59 +0000826class PimpPackage_source(PimpPackage):
827
Jack Jansen0ae32202003-04-09 13:25:43 +0000828 def unpackPackageOnly(self, output=None):
829 """Unpack a source package and check that setup.py exists"""
830 PimpPackage.unpackPackageOnly(self, output)
831 # Test that a setup script has been create
832 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
833 setupname = os.path.join(self._buildDirname, "setup.py")
834 if not os.path.exists(setupname) and not NO_EXECUTE:
835 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000836
Jack Jansen0ae32202003-04-09 13:25:43 +0000837 def installPackageOnly(self, output=None):
838 """Install a single source package.
839
840 If output is given it should be a file-like object and it
841 will receive a log of what happened."""
842
843 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000844 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000845 return "pre-install %s: running \"%s\" failed" % \
846 (self.fullname(), self._dict['Pre-install-command'])
847
848 self.beforeInstall()
849 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000850 if installcmd and self._install_renames:
851 return "Package has install-command and can only be installed to standard location"
852 # This is the "bit-bucket" for installations: everything we don't
853 # want. After installation we check that it is actually empty
854 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000855 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000856 extra_args = ""
857 for k, v in self._db.preferences.installLocations:
858 if not v:
859 # We don't want these files installed. Send them
860 # to the bit-bucket.
861 if not unwanted_install_dir:
862 unwanted_install_dir = tempfile.mkdtemp()
863 v = unwanted_install_dir
864 extra_args = extra_args + " %s \"%s\"" % (k, v)
865 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
866 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000867 return "install %s: running \"%s\" failed" % \
868 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000869 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
870 unwanted_files = os.listdir(unwanted_install_dir)
871 if unwanted_files:
872 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
873 else:
874 rv = None
875 shutil.rmtree(unwanted_install_dir)
876 return rv
Jack Jansen0ae32202003-04-09 13:25:43 +0000877
878 self.afterInstall()
879
880 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000881 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000882 return "post-install %s: running \"%s\" failed" % \
883 (self.fullname(), self._dict['Post-install-command'])
884 return None
885
886
Jack Jansen95839b82003-02-09 23:10:20 +0000887class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000888 """Installer engine: computes dependencies and installs
889 packages in the right order."""
890
891 def __init__(self, db):
892 self._todo = []
893 self._db = db
894 self._curtodo = []
895 self._curmessages = []
896
897 def __contains__(self, package):
898 return package in self._todo
899
900 def _addPackages(self, packages):
901 for package in packages:
902 if not package in self._todo:
903 self._todo.insert(0, package)
904
905 def _prepareInstall(self, package, force=0, recursive=1):
906 """Internal routine, recursive engine for prepareInstall.
907
908 Test whether the package is installed and (if not installed
909 or if force==1) prepend it to the temporary todo list and
910 call ourselves recursively on all prerequisites."""
911
912 if not force:
913 status, message = package.installed()
914 if status == "yes":
915 return
916 if package in self._todo or package in self._curtodo:
917 return
918 self._curtodo.insert(0, package)
919 if not recursive:
920 return
921 prereqs = package.prerequisites()
922 for pkg, descr in prereqs:
923 if pkg:
924 self._prepareInstall(pkg, force, recursive)
925 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000926 self._curmessages.append("Problem with dependency: %s" % descr)
Jack Jansen0ae32202003-04-09 13:25:43 +0000927
928 def prepareInstall(self, package, force=0, recursive=1):
929 """Prepare installation of a package.
930
931 If the package is already installed and force is false nothing
932 is done. If recursive is true prerequisites are installed first.
933
934 Returns a list of packages (to be passed to install) and a list
935 of messages of any problems encountered.
936 """
937
938 self._curtodo = []
939 self._curmessages = []
940 self._prepareInstall(package, force, recursive)
941 rv = self._curtodo, self._curmessages
942 self._curtodo = []
943 self._curmessages = []
944 return rv
945
946 def install(self, packages, output):
947 """Install a list of packages."""
948
949 self._addPackages(packages)
950 status = []
951 for pkg in self._todo:
952 msg = pkg.installSinglePackage(output)
953 if msg:
954 status.append(msg)
955 return status
956
957
958
Jack Jansen989ddc02004-03-11 23:03:59 +0000959def _run(mode, verbose, force, args, prefargs, watcher):
Jack Jansen0ae32202003-04-09 13:25:43 +0000960 """Engine for the main program"""
961
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000962 prefs = PimpPreferences(**prefargs)
Jack Jansen989ddc02004-03-11 23:03:59 +0000963 if watcher:
964 prefs.setWatcher(watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000965 rv = prefs.check()
966 if rv:
967 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000968 db = PimpDatabase(prefs)
969 db.appendURL(prefs.pimpDatabase)
970
971 if mode == 'dump':
972 db.dump(sys.stdout)
973 elif mode =='list':
974 if not args:
975 args = db.listnames()
976 print "%-20.20s\t%s" % ("Package", "Description")
977 print
978 for pkgname in args:
979 pkg = db.find(pkgname)
980 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000981 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000982 pkgname = pkg.fullname()
983 else:
984 description = 'Error: no such package'
985 print "%-20.20s\t%s" % (pkgname, description)
986 if verbose:
987 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000988 try:
989 print "\tDownload URL:\t", pkg.downloadURL()
990 except KeyError:
991 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +0000992 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000993 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +0000994 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +0000995 elif mode =='status':
996 if not args:
997 args = db.listnames()
998 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
999 print
1000 for pkgname in args:
1001 pkg = db.find(pkgname)
1002 if pkg:
1003 status, msg = pkg.installed()
1004 pkgname = pkg.fullname()
1005 else:
1006 status = 'error'
1007 msg = 'No such package'
1008 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
1009 if verbose and status == "no":
1010 prereq = pkg.prerequisites()
1011 for pkg, msg in prereq:
1012 if not pkg:
1013 pkg = ''
1014 else:
1015 pkg = pkg.fullname()
1016 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
1017 elif mode == 'install':
1018 if not args:
1019 print 'Please specify packages to install'
1020 sys.exit(1)
1021 inst = PimpInstaller(db)
1022 for pkgname in args:
1023 pkg = db.find(pkgname)
1024 if not pkg:
1025 print '%s: No such package' % pkgname
1026 continue
1027 list, messages = inst.prepareInstall(pkg, force)
1028 if messages and not force:
1029 print "%s: Not installed:" % pkgname
1030 for m in messages:
1031 print "\t", m
1032 else:
1033 if verbose:
1034 output = sys.stdout
1035 else:
1036 output = None
1037 messages = inst.install(list, output)
1038 if messages:
1039 print "%s: Not installed:" % pkgname
1040 for m in messages:
1041 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +00001042
1043def main():
Jack Jansen0ae32202003-04-09 13:25:43 +00001044 """Minimal commandline tool to drive pimp."""
1045
1046 import getopt
1047 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001048 print "Usage: pimp [options] -s [package ...] List installed status"
1049 print " pimp [options] -l [package ...] Show package information"
1050 print " pimp [options] -i package ... Install packages"
1051 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +00001052 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +00001053 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001054 print " -v Verbose"
1055 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +00001056 print " -D dir Set destination directory"
1057 print " (default: %s)" % DEFAULT_INSTALLDIR
1058 print " -u url URL for database"
Jack Jansen0ae32202003-04-09 13:25:43 +00001059 sys.exit(1)
1060
Jack Jansen989ddc02004-03-11 23:03:59 +00001061 class _Watcher:
1062 def update(self, msg):
1063 sys.stderr.write(msg + '\r')
1064 return 1
1065
Jack Jansen0ae32202003-04-09 13:25:43 +00001066 try:
Jack Jansenb789a062003-05-28 18:56:30 +00001067 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
1068 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +00001069 _help()
1070 if not opts and not args:
1071 _help()
1072 mode = None
1073 force = 0
1074 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001075 prefargs = {}
Jack Jansen989ddc02004-03-11 23:03:59 +00001076 watcher = None
Jack Jansen0ae32202003-04-09 13:25:43 +00001077 for o, a in opts:
1078 if o == '-s':
1079 if mode:
1080 _help()
1081 mode = 'status'
1082 if o == '-l':
1083 if mode:
1084 _help()
1085 mode = 'list'
1086 if o == '-d':
1087 if mode:
1088 _help()
1089 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001090 if o == '-V':
1091 if mode:
1092 _help()
1093 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001094 if o == '-i':
1095 mode = 'install'
1096 if o == '-f':
1097 force = 1
1098 if o == '-v':
1099 verbose = 1
Jack Jansen989ddc02004-03-11 23:03:59 +00001100 watcher = _Watcher()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001101 if o == '-D':
1102 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001103 if o == '-u':
1104 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001105 if not mode:
1106 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001107 if mode == 'version':
1108 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1109 else:
Jack Jansen989ddc02004-03-11 23:03:59 +00001110 _run(mode, verbose, force, args, prefargs, watcher)
Jack Jansenb789a062003-05-28 18:56:30 +00001111
1112# Finally, try to update ourselves to a newer version.
1113# If the end-user updates pimp through pimp the new version
1114# will be called pimp_update and live in site-packages
1115# or somewhere similar
1116if __name__ != 'pimp_update':
1117 try:
1118 import pimp_update
1119 except ImportError:
1120 pass
1121 else:
1122 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1123 import warnings
1124 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1125 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1126 else:
1127 from pimp_update import *
1128
Jack Jansen95839b82003-02-09 23:10:20 +00001129if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001130 main()
1131
1132