blob: 67fab7607dc55b36b34bf6a254711c2bf9e057b7 [file] [log] [blame]
Jack Jansen6a600ab2003-02-10 15:55:51 +00001"""Package Install Manager for Python.
2
Tim Peters182b5ac2004-07-18 06:16:08 +00003This 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
Tim Peters182b5ac2004-07-18 06:16:08 +000030__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main",
Jack Jansenb789a062003-05-28 18:56:30 +000031 "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"
Tim Peters182b5ac2004-07-18 06:16:08 +000055
Jack Jansen989ddc02004-03-11 23:03:59 +000056 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)
Tim Peters182b5ac2004-07-18 06:16:08 +000060
Jack Jansen989ddc02004-03-11 23:03:59 +000061 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."""
Tim Peters182b5ac2004-07-18 06:16:08 +000097
Jack Jansen6fde1ce2003-04-15 14:43:05 +000098 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"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000115
Jack Jansen989ddc02004-03-11 23:03:59 +0000116 def __init__(self, argument,
117 dir="",
118 watcher=None):
119 self.argument = argument
120 self._dir = dir
121 self._watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000122
Jack Jansen989ddc02004-03-11 23:03:59 +0000123 def download(self, url, filename, output=None):
124 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000125
Jack Jansen989ddc02004-03-11 23:03:59 +0000126 def update(self, str):
127 if self._watcher:
128 return self._watcher.update(str)
129 return True
Tim Peters182b5ac2004-07-18 06:16:08 +0000130
Jack Jansen989ddc02004-03-11 23:03:59 +0000131class 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)
Tim Peters182b5ac2004-07-18 06:16:08 +0000141
Jack Jansen989ddc02004-03-11 23:03:59 +0000142class 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
Tim Peters182b5ac2004-07-18 06:16:08 +0000153
Jack Jansen989ddc02004-03-11 23:03:59 +0000154 data = download.read(4096) #read 4K at a time
155 dlsize = 0
156 lasttime = 0
157 while keepgoing:
158 dlsize = dlsize + len(data)
Tim Peters182b5ac2004-07-18 06:16:08 +0000159 if len(data) == 0:
Jack Jansen989ddc02004-03-11 23:03:59 +0000160 #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
Tim Peters182b5ac2004-07-18 06:16:08 +0000174
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000175class PimpUnpacker:
176 """Abstract base class - Unpacker for archives"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000177
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000178 _can_rename = False
Tim Peters182b5ac2004-07-18 06:16:08 +0000179
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000180 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
Tim Peters182b5ac2004-07-18 06:16:08 +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
Tim Peters182b5ac2004-07-18 06:16:08 +0000193
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
Tim Peters182b5ac2004-07-18 06:16:08 +0000198
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000199class PimpCommandUnpacker(PimpUnpacker):
200 """Unpack archives by calling a Unix utility"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000201
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000202 _can_rename = False
Tim Peters182b5ac2004-07-18 06:16:08 +0000203
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"
Tim Peters182b5ac2004-07-18 06:16:08 +0000208
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000209class PimpTarUnpacker(PimpUnpacker):
210 """Unpack tarfiles using the builtin tarfile module"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000211
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000212 _can_rename = True
Tim Peters182b5ac2004-07-18 06:16:08 +0000213
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)
Tim Peters182b5ac2004-07-18 06:16:08 +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."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000269
270 def __init__(self,
Jack Jansen0ae32202003-04-09 13:25:43 +0000271 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
Tim Peters182b5ac2004-07-18 06:16:08 +0000290
Jack Jansen989ddc02004-03-11 23:03:59 +0000291 def setWatcher(self, watcher):
292 self.watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000293
Jack Jansen20fa6752003-04-16 12:15:34 +0000294 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
Tim Peters182b5ac2004-07-18 06:16:08 +0000306
Jack Jansen5da131b2003-06-01 20:57:12 +0000307 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."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000313
Jack Jansen0ae32202003-04-09 13:25:43 +0000314 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
Tim Peters182b5ac2004-07-18 06:16:08 +0000340
Jack Jansen0ae32202003-04-09 13:25:43 +0000341 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)
Tim Peters182b5ac2004-07-18 06:16:08 +0000351
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."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000357
Jack Jansen0ae32202003-04-09 13:25:43 +0000358 def __init__(self, prefs):
359 self._packages = []
360 self.preferences = prefs
Jack Jansen118e1272004-03-13 23:50:48 +0000361 self._url = ""
Jack Jansen0ae32202003-04-09 13:25:43 +0000362 self._urllist = []
363 self._version = ""
364 self._maintainer = ""
365 self._description = ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000366
Jack Jansen118e1272004-03-13 23:50:48 +0000367 # Accessor functions
368 def url(self): return self._url
369 def version(self): return self._version
370 def maintainer(self): return self._maintainer
371 def description(self): return self._description
Tim Peters182b5ac2004-07-18 06:16:08 +0000372
Jack Jansen0ae32202003-04-09 13:25:43 +0000373 def close(self):
374 """Clean up"""
375 self._packages = []
376 self.preferences = None
Tim Peters182b5ac2004-07-18 06:16:08 +0000377
Jack Jansen0ae32202003-04-09 13:25:43 +0000378 def appendURL(self, url, included=0):
379 """Append packages from the database with the given URL.
380 Only the first database should specify included=0, so the
381 global information (maintainer, description) get stored."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000382
Jack Jansen0ae32202003-04-09 13:25:43 +0000383 if url in self._urllist:
384 return
385 self._urllist.append(url)
386 fp = urllib2.urlopen(url).fp
Jack Jansenc32cec12004-03-13 23:32:47 +0000387 plistdata = plistlib.Plist.fromFile(fp)
Jack Jansen0ae32202003-04-09 13:25:43 +0000388 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000389 if included:
Jack Jansenc32cec12004-03-13 23:32:47 +0000390 version = plistdata.get('Version')
Jack Jansenb789a062003-05-28 18:56:30 +0000391 if version and version > self._version:
392 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
393 (url, version))
394 else:
Jack Jansenc32cec12004-03-13 23:32:47 +0000395 self._version = plistdata.get('Version')
Jack Jansenb789a062003-05-28 18:56:30 +0000396 if not self._version:
397 sys.stderr.write("Warning: database has no Version information\n")
398 elif self._version > PIMP_VERSION:
Tim Peters182b5ac2004-07-18 06:16:08 +0000399 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000400 % (self._version, PIMP_VERSION))
Jack Jansenc32cec12004-03-13 23:32:47 +0000401 self._maintainer = plistdata.get('Maintainer', '')
402 self._description = plistdata.get('Description', '').strip()
Jack Jansen118e1272004-03-13 23:50:48 +0000403 self._url = url
Jack Jansenc32cec12004-03-13 23:32:47 +0000404 self._appendPackages(plistdata['Packages'])
405 others = plistdata.get('Include', [])
Jack Jansen0ae32202003-04-09 13:25:43 +0000406 for url in others:
407 self.appendURL(url, included=1)
Tim Peters182b5ac2004-07-18 06:16:08 +0000408
Jack Jansen0ae32202003-04-09 13:25:43 +0000409 def _appendPackages(self, packages):
410 """Given a list of dictionaries containing package
411 descriptions create the PimpPackage objects and append them
412 to our internal storage."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000413
Jack Jansen0ae32202003-04-09 13:25:43 +0000414 for p in packages:
415 p = dict(p)
416 flavor = p.get('Flavor')
417 if flavor == 'source':
418 pkg = PimpPackage_source(self, p)
419 elif flavor == 'binary':
420 pkg = PimpPackage_binary(self, p)
421 else:
422 pkg = PimpPackage(self, dict(p))
423 self._packages.append(pkg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000424
Jack Jansen0ae32202003-04-09 13:25:43 +0000425 def list(self):
426 """Return a list of all PimpPackage objects in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000427
Jack Jansen0ae32202003-04-09 13:25:43 +0000428 return self._packages
Tim Peters182b5ac2004-07-18 06:16:08 +0000429
Jack Jansen0ae32202003-04-09 13:25:43 +0000430 def listnames(self):
431 """Return a list of names of all packages in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000432
Jack Jansen0ae32202003-04-09 13:25:43 +0000433 rv = []
434 for pkg in self._packages:
435 rv.append(pkg.fullname())
436 rv.sort()
437 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000438
Jack Jansen0ae32202003-04-09 13:25:43 +0000439 def dump(self, pathOrFile):
440 """Dump the contents of the database to an XML .plist file.
Tim Peters182b5ac2004-07-18 06:16:08 +0000441
Jack Jansen0ae32202003-04-09 13:25:43 +0000442 The file can be passed as either a file object or a pathname.
443 All data, including included databases, is dumped."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000444
Jack Jansen0ae32202003-04-09 13:25:43 +0000445 packages = []
446 for pkg in self._packages:
447 packages.append(pkg.dump())
Jack Jansenc32cec12004-03-13 23:32:47 +0000448 plistdata = {
Jack Jansen0ae32202003-04-09 13:25:43 +0000449 'Version': self._version,
450 'Maintainer': self._maintainer,
451 'Description': self._description,
452 'Packages': packages
453 }
Jack Jansenc32cec12004-03-13 23:32:47 +0000454 plist = plistlib.Plist(**plistdata)
Jack Jansen0ae32202003-04-09 13:25:43 +0000455 plist.write(pathOrFile)
Tim Peters182b5ac2004-07-18 06:16:08 +0000456
Jack Jansen0ae32202003-04-09 13:25:43 +0000457 def find(self, ident):
458 """Find a package. The package can be specified by name
459 or as a dictionary with name, version and flavor entries.
Tim Peters182b5ac2004-07-18 06:16:08 +0000460
Jack Jansen0ae32202003-04-09 13:25:43 +0000461 Only name is obligatory. If there are multiple matches the
462 best one (higher version number, flavors ordered according to
463 users' preference) is returned."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000464
Jack Jansen0ae32202003-04-09 13:25:43 +0000465 if type(ident) == str:
466 # Remove ( and ) for pseudo-packages
467 if ident[0] == '(' and ident[-1] == ')':
468 ident = ident[1:-1]
469 # Split into name-version-flavor
470 fields = ident.split('-')
471 if len(fields) < 1 or len(fields) > 3:
472 return None
473 name = fields[0]
474 if len(fields) > 1:
475 version = fields[1]
476 else:
477 version = None
478 if len(fields) > 2:
479 flavor = fields[2]
480 else:
481 flavor = None
482 else:
483 name = ident['Name']
484 version = ident.get('Version')
485 flavor = ident.get('Flavor')
486 found = None
487 for p in self._packages:
488 if name == p.name() and \
489 (not version or version == p.version()) and \
490 (not flavor or flavor == p.flavor()):
491 if not found or found < p:
492 found = p
493 return found
Tim Peters182b5ac2004-07-18 06:16:08 +0000494
Jack Jansene7b33db2003-02-11 22:40:59 +0000495ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000496 "Name",
497 "Version",
498 "Flavor",
499 "Description",
500 "Home-page",
501 "Download-URL",
502 "Install-test",
503 "Install-command",
504 "Pre-install-command",
505 "Post-install-command",
506 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000507 "MD5Sum",
508 "User-install-skips",
509 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000510]
511
Jack Jansen95839b82003-02-09 23:10:20 +0000512class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000513 """Class representing a single package."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000514
Jack Jansenc32cec12004-03-13 23:32:47 +0000515 def __init__(self, db, plistdata):
Jack Jansen0ae32202003-04-09 13:25:43 +0000516 self._db = db
Jack Jansenc32cec12004-03-13 23:32:47 +0000517 name = plistdata["Name"]
518 for k in plistdata.keys():
Jack Jansen0ae32202003-04-09 13:25:43 +0000519 if not k in ALLOWED_KEYS:
520 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
Jack Jansenc32cec12004-03-13 23:32:47 +0000521 self._dict = plistdata
Tim Peters182b5ac2004-07-18 06:16:08 +0000522
Jack Jansen0ae32202003-04-09 13:25:43 +0000523 def __getitem__(self, key):
524 return self._dict[key]
Tim Peters182b5ac2004-07-18 06:16:08 +0000525
Jack Jansen0ae32202003-04-09 13:25:43 +0000526 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000527 def version(self): return self._dict.get('Version')
528 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000529 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000530 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000531 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000532 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000533 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Tim Peters182b5ac2004-07-18 06:16:08 +0000534
Jack Jansen0ae32202003-04-09 13:25:43 +0000535 def fullname(self):
536 """Return the full name "name-version-flavor" of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000537
Jack Jansen0ae32202003-04-09 13:25:43 +0000538 If the package is a pseudo-package, something that cannot be
539 installed through pimp, return the name in (parentheses)."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000540
Jack Jansen0ae32202003-04-09 13:25:43 +0000541 rv = self._dict['Name']
542 if self._dict.has_key('Version'):
543 rv = rv + '-%s' % self._dict['Version']
544 if self._dict.has_key('Flavor'):
545 rv = rv + '-%s' % self._dict['Flavor']
546 if not self._dict.get('Download-URL'):
547 # Pseudo-package, show in parentheses
548 rv = '(%s)' % rv
549 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000550
Jack Jansen0ae32202003-04-09 13:25:43 +0000551 def dump(self):
552 """Return a dict object containing the information on the package."""
553 return self._dict
Tim Peters182b5ac2004-07-18 06:16:08 +0000554
Jack Jansen0ae32202003-04-09 13:25:43 +0000555 def __cmp__(self, other):
556 """Compare two packages, where the "better" package sorts lower."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000557
Jack Jansen0ae32202003-04-09 13:25:43 +0000558 if not isinstance(other, PimpPackage):
559 return cmp(id(self), id(other))
560 if self.name() != other.name():
561 return cmp(self.name(), other.name())
562 if self.version() != other.version():
563 return -cmp(self.version(), other.version())
564 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Tim Peters182b5ac2004-07-18 06:16:08 +0000565
Jack Jansen0ae32202003-04-09 13:25:43 +0000566 def installed(self):
567 """Test wheter the package is installed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000568
Jack Jansen0ae32202003-04-09 13:25:43 +0000569 Returns two values: a status indicator which is one of
570 "yes", "no", "old" (an older version is installed) or "bad"
571 (something went wrong during the install test) and a human
572 readable string which may contain more details."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000573
Jack Jansen0ae32202003-04-09 13:25:43 +0000574 namespace = {
575 "NotInstalled": _scriptExc_NotInstalled,
576 "OldInstalled": _scriptExc_OldInstalled,
577 "BadInstalled": _scriptExc_BadInstalled,
578 "os": os,
579 "sys": sys,
580 }
581 installTest = self._dict['Install-test'].strip() + '\n'
582 try:
583 exec installTest in namespace
584 except ImportError, arg:
585 return "no", str(arg)
586 except _scriptExc_NotInstalled, arg:
587 return "no", str(arg)
588 except _scriptExc_OldInstalled, arg:
589 return "old", str(arg)
590 except _scriptExc_BadInstalled, arg:
591 return "bad", str(arg)
592 except:
593 sys.stderr.write("-------------------------------------\n")
594 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
595 sys.stderr.write("---- source:\n")
596 sys.stderr.write(installTest)
597 sys.stderr.write("---- exception:\n")
598 import traceback
599 traceback.print_exc(file=sys.stderr)
600 if self._db._maintainer:
601 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
602 sys.stderr.write("-------------------------------------\n")
603 return "bad", "Package install test got exception"
604 return "yes", ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000605
Jack Jansen0ae32202003-04-09 13:25:43 +0000606 def prerequisites(self):
607 """Return a list of prerequisites for this package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000608
Jack Jansen0ae32202003-04-09 13:25:43 +0000609 The list contains 2-tuples, of which the first item is either
610 a PimpPackage object or None, and the second is a descriptive
611 string. The first item can be None if this package depends on
612 something that isn't pimp-installable, in which case the descriptive
613 string should tell the user what to do."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000614
Jack Jansen0ae32202003-04-09 13:25:43 +0000615 rv = []
616 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000617 # For pseudo-packages that are already installed we don't
618 # return an error message
619 status, _ = self.installed()
620 if status == "yes":
621 return []
Tim Peters182b5ac2004-07-18 06:16:08 +0000622 return [(None,
Jack Jansen20fa6752003-04-16 12:15:34 +0000623 "%s: This package cannot be installed automatically (no Download-URL field)" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000624 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000625 if self.systemwideOnly() and self._db.preferences.isUserInstall():
626 return [(None,
627 "%s: This package can only be installed system-wide" %
628 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000629 if not self._dict.get('Prerequisites'):
630 return []
631 for item in self._dict['Prerequisites']:
632 if type(item) == str:
633 pkg = None
634 descr = str(item)
635 else:
636 name = item['Name']
637 if item.has_key('Version'):
638 name = name + '-' + item['Version']
639 if item.has_key('Flavor'):
640 name = name + '-' + item['Flavor']
641 pkg = self._db.find(name)
642 if not pkg:
643 descr = "Requires unknown %s"%name
644 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000645 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000646 rv.append((pkg, descr))
647 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000648
649
Jack Jansen0ae32202003-04-09 13:25:43 +0000650 def downloadPackageOnly(self, output=None):
651 """Download a single package, if needed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000652
Jack Jansen0ae32202003-04-09 13:25:43 +0000653 An MD5 signature is used to determine whether download is needed,
654 and to test that we actually downloaded what we expected.
655 If output is given it is a file-like object that will receive a log
656 of what happens.
Tim Peters182b5ac2004-07-18 06:16:08 +0000657
Jack Jansen0ae32202003-04-09 13:25:43 +0000658 If anything unforeseen happened the method returns an error message
659 string.
660 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000661
Jack Jansen0ae32202003-04-09 13:25:43 +0000662 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
663 path = urllib.url2pathname(path)
664 filename = os.path.split(path)[1]
Tim Peters182b5ac2004-07-18 06:16:08 +0000665 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
Jack Jansen0ae32202003-04-09 13:25:43 +0000666 if not self._archiveOK():
667 if scheme == 'manual':
668 return "Please download package manually and save as %s" % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000669 downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000670 watcher=self._db.preferences.watcher)
671 if not downloader.download(self._dict['Download-URL'],
672 self.archiveFilename, output):
Jack Jansen0ae32202003-04-09 13:25:43 +0000673 return "download command failed"
674 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
675 return "archive not found after download"
676 if not self._archiveOK():
677 return "archive does not have correct MD5 checksum"
Tim Peters182b5ac2004-07-18 06:16:08 +0000678
Jack Jansen0ae32202003-04-09 13:25:43 +0000679 def _archiveOK(self):
680 """Test an archive. It should exist and the MD5 checksum should be correct."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000681
Jack Jansen0ae32202003-04-09 13:25:43 +0000682 if not os.path.exists(self.archiveFilename):
683 return 0
684 if not self._dict.get('MD5Sum'):
685 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
686 return 1
687 data = open(self.archiveFilename, 'rb').read()
688 checksum = md5.new(data).hexdigest()
689 return checksum == self._dict['MD5Sum']
Tim Peters182b5ac2004-07-18 06:16:08 +0000690
Jack Jansen0ae32202003-04-09 13:25:43 +0000691 def unpackPackageOnly(self, output=None):
692 """Unpack a downloaded package archive."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000693
Jack Jansen0ae32202003-04-09 13:25:43 +0000694 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000695 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000696 if filename[-len(ext):] == ext:
697 break
698 else:
699 return "unknown extension for archive file: %s" % filename
700 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000701 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000702 watcher=self._db.preferences.watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000703 rv = unpacker.unpack(self.archiveFilename, output=output)
704 if rv:
705 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000706
Jack Jansen0ae32202003-04-09 13:25:43 +0000707 def installPackageOnly(self, output=None):
708 """Default install method, to be overridden by subclasses"""
709 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
710 % (self.fullname(), self._dict.get(flavor, ""))
Tim Peters182b5ac2004-07-18 06:16:08 +0000711
Jack Jansen0ae32202003-04-09 13:25:43 +0000712 def installSinglePackage(self, output=None):
713 """Download, unpack and install a single package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000714
Jack Jansen0ae32202003-04-09 13:25:43 +0000715 If output is given it should be a file-like object and it
716 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000717
Jack Jansen749f4812003-07-21 20:47:11 +0000718 if not self._dict.get('Download-URL'):
719 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000720 msg = self.downloadPackageOnly(output)
721 if msg:
722 return "%s: download: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000723
Jack Jansen0ae32202003-04-09 13:25:43 +0000724 msg = self.unpackPackageOnly(output)
725 if msg:
726 return "%s: unpack: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000727
Jack Jansen0ae32202003-04-09 13:25:43 +0000728 return self.installPackageOnly(output)
Tim Peters182b5ac2004-07-18 06:16:08 +0000729
Jack Jansen0ae32202003-04-09 13:25:43 +0000730 def beforeInstall(self):
731 """Bookkeeping before installation: remember what we have in site-packages"""
732 self._old_contents = os.listdir(self._db.preferences.installDir)
Tim Peters182b5ac2004-07-18 06:16:08 +0000733
Jack Jansen0ae32202003-04-09 13:25:43 +0000734 def afterInstall(self):
735 """Bookkeeping after installation: interpret any new .pth files that have
736 appeared"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000737
Jack Jansen0ae32202003-04-09 13:25:43 +0000738 new_contents = os.listdir(self._db.preferences.installDir)
739 for fn in new_contents:
740 if fn in self._old_contents:
741 continue
742 if fn[-4:] != '.pth':
743 continue
744 fullname = os.path.join(self._db.preferences.installDir, fn)
745 f = open(fullname)
746 for line in f.readlines():
747 if not line:
748 continue
749 if line[0] == '#':
750 continue
751 if line[:6] == 'import':
752 exec line
753 continue
754 if line[-1] == '\n':
755 line = line[:-1]
756 if not os.path.isabs(line):
757 line = os.path.join(self._db.preferences.installDir, line)
758 line = os.path.realpath(line)
759 if not line in sys.path:
Tim Peters182b5ac2004-07-18 06:16:08 +0000760 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000761
Jack Jansen5da131b2003-06-01 20:57:12 +0000762 def filterExpectedSkips(self, names):
763 """Return a list that contains only unpexpected skips"""
764 if not self._db.preferences.isUserInstall():
765 return names
766 expected_skips = self._dict.get('User-install-skips')
767 if not expected_skips:
768 return names
769 newnames = []
770 for name in names:
771 for skip in expected_skips:
772 if name[:len(skip)] == skip:
773 break
774 else:
775 newnames.append(name)
776 return newnames
777
Jack Jansen0dacac42003-02-14 14:11:59 +0000778class PimpPackage_binary(PimpPackage):
779
Jack Jansen0ae32202003-04-09 13:25:43 +0000780 def unpackPackageOnly(self, output=None):
781 """We don't unpack binary packages until installing"""
782 pass
Tim Peters182b5ac2004-07-18 06:16:08 +0000783
Jack Jansen0ae32202003-04-09 13:25:43 +0000784 def installPackageOnly(self, output=None):
785 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000786
Jack Jansen0ae32202003-04-09 13:25:43 +0000787 If output is given it should be a file-like object and it
788 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000789
Jack Jansen0ae32202003-04-09 13:25:43 +0000790 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000791 return "%s: Binary package cannot have Install-command" % self.fullname()
Tim Peters182b5ac2004-07-18 06:16:08 +0000792
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000793 if self._dict.has_key('Pre-install-command'):
794 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
795 return "pre-install %s: running \"%s\" failed" % \
796 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000797
Jack Jansen0ae32202003-04-09 13:25:43 +0000798 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000799
Jack Jansen0ae32202003-04-09 13:25:43 +0000800 # Install by unpacking
801 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000802 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000803 if filename[-len(ext):] == ext:
804 break
805 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000806 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
807 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000808
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000809 install_renames = []
810 for k, newloc in self._db.preferences.installLocations:
811 if not newloc:
812 continue
813 if k == "--install-lib":
814 oldloc = DEFAULT_INSTALLDIR
815 else:
816 return "%s: Don't know installLocation %s" % (self.fullname(), k)
817 install_renames.append((oldloc, newloc))
Tim Peters182b5ac2004-07-18 06:16:08 +0000818
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000819 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000820 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000821 if rv:
822 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000823
Jack Jansen0ae32202003-04-09 13:25:43 +0000824 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000825
Jack Jansen0ae32202003-04-09 13:25:43 +0000826 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000827 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
828 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000829 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000830
Jack Jansen0ae32202003-04-09 13:25:43 +0000831 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000832
833
Jack Jansen0dacac42003-02-14 14:11:59 +0000834class PimpPackage_source(PimpPackage):
835
Jack Jansen0ae32202003-04-09 13:25:43 +0000836 def unpackPackageOnly(self, output=None):
837 """Unpack a source package and check that setup.py exists"""
838 PimpPackage.unpackPackageOnly(self, output)
839 # Test that a setup script has been create
840 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
841 setupname = os.path.join(self._buildDirname, "setup.py")
842 if not os.path.exists(setupname) and not NO_EXECUTE:
843 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000844
Jack Jansen0ae32202003-04-09 13:25:43 +0000845 def installPackageOnly(self, output=None):
846 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000847
Jack Jansen0ae32202003-04-09 13:25:43 +0000848 If output is given it should be a file-like object and it
849 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000850
Jack Jansen0ae32202003-04-09 13:25:43 +0000851 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000852 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000853 return "pre-install %s: running \"%s\" failed" % \
854 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000855
Jack Jansen0ae32202003-04-09 13:25:43 +0000856 self.beforeInstall()
857 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000858 if installcmd and self._install_renames:
859 return "Package has install-command and can only be installed to standard location"
860 # This is the "bit-bucket" for installations: everything we don't
861 # want. After installation we check that it is actually empty
862 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000863 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000864 extra_args = ""
865 for k, v in self._db.preferences.installLocations:
866 if not v:
867 # We don't want these files installed. Send them
868 # to the bit-bucket.
869 if not unwanted_install_dir:
870 unwanted_install_dir = tempfile.mkdtemp()
871 v = unwanted_install_dir
872 extra_args = extra_args + " %s \"%s\"" % (k, v)
873 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
874 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000875 return "install %s: running \"%s\" failed" % \
876 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000877 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
878 unwanted_files = os.listdir(unwanted_install_dir)
879 if unwanted_files:
880 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
881 else:
882 rv = None
883 shutil.rmtree(unwanted_install_dir)
884 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000885
Jack Jansen0ae32202003-04-09 13:25:43 +0000886 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000887
Jack Jansen0ae32202003-04-09 13:25:43 +0000888 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000889 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000890 return "post-install %s: running \"%s\" failed" % \
891 (self.fullname(), self._dict['Post-install-command'])
892 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000893
894
Jack Jansen95839b82003-02-09 23:10:20 +0000895class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000896 """Installer engine: computes dependencies and installs
897 packages in the right order."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000898
Jack Jansen0ae32202003-04-09 13:25:43 +0000899 def __init__(self, db):
900 self._todo = []
901 self._db = db
902 self._curtodo = []
903 self._curmessages = []
Tim Peters182b5ac2004-07-18 06:16:08 +0000904
Jack Jansen0ae32202003-04-09 13:25:43 +0000905 def __contains__(self, package):
906 return package in self._todo
Tim Peters182b5ac2004-07-18 06:16:08 +0000907
Jack Jansen0ae32202003-04-09 13:25:43 +0000908 def _addPackages(self, packages):
909 for package in packages:
910 if not package in self._todo:
Jack Jansen118e1272004-03-13 23:50:48 +0000911 self._todo.append(package)
Tim Peters182b5ac2004-07-18 06:16:08 +0000912
Jack Jansen0ae32202003-04-09 13:25:43 +0000913 def _prepareInstall(self, package, force=0, recursive=1):
914 """Internal routine, recursive engine for prepareInstall.
Tim Peters182b5ac2004-07-18 06:16:08 +0000915
Jack Jansen0ae32202003-04-09 13:25:43 +0000916 Test whether the package is installed and (if not installed
917 or if force==1) prepend it to the temporary todo list and
918 call ourselves recursively on all prerequisites."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000919
Jack Jansen0ae32202003-04-09 13:25:43 +0000920 if not force:
921 status, message = package.installed()
922 if status == "yes":
Tim Peters182b5ac2004-07-18 06:16:08 +0000923 return
Jack Jansen0ae32202003-04-09 13:25:43 +0000924 if package in self._todo or package in self._curtodo:
925 return
926 self._curtodo.insert(0, package)
927 if not recursive:
928 return
929 prereqs = package.prerequisites()
930 for pkg, descr in prereqs:
931 if pkg:
Jack Jansen0576d0a2004-03-13 23:03:38 +0000932 self._prepareInstall(pkg, False, recursive)
Jack Jansen0ae32202003-04-09 13:25:43 +0000933 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000934 self._curmessages.append("Problem with dependency: %s" % descr)
Tim Peters182b5ac2004-07-18 06:16:08 +0000935
Jack Jansen0ae32202003-04-09 13:25:43 +0000936 def prepareInstall(self, package, force=0, recursive=1):
937 """Prepare installation of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000938
Jack Jansen0ae32202003-04-09 13:25:43 +0000939 If the package is already installed and force is false nothing
940 is done. If recursive is true prerequisites are installed first.
Tim Peters182b5ac2004-07-18 06:16:08 +0000941
Jack Jansen0ae32202003-04-09 13:25:43 +0000942 Returns a list of packages (to be passed to install) and a list
943 of messages of any problems encountered.
944 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000945
Jack Jansen0ae32202003-04-09 13:25:43 +0000946 self._curtodo = []
947 self._curmessages = []
948 self._prepareInstall(package, force, recursive)
949 rv = self._curtodo, self._curmessages
950 self._curtodo = []
951 self._curmessages = []
952 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000953
Jack Jansen0ae32202003-04-09 13:25:43 +0000954 def install(self, packages, output):
955 """Install a list of packages."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000956
Jack Jansen0ae32202003-04-09 13:25:43 +0000957 self._addPackages(packages)
958 status = []
959 for pkg in self._todo:
960 msg = pkg.installSinglePackage(output)
961 if msg:
962 status.append(msg)
963 return status
Tim Peters182b5ac2004-07-18 06:16:08 +0000964
965
966
Jack Jansen989ddc02004-03-11 23:03:59 +0000967def _run(mode, verbose, force, args, prefargs, watcher):
Jack Jansen0ae32202003-04-09 13:25:43 +0000968 """Engine for the main program"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000969
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000970 prefs = PimpPreferences(**prefargs)
Jack Jansen989ddc02004-03-11 23:03:59 +0000971 if watcher:
972 prefs.setWatcher(watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000973 rv = prefs.check()
974 if rv:
975 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +0000976 db = PimpDatabase(prefs)
977 db.appendURL(prefs.pimpDatabase)
Tim Peters182b5ac2004-07-18 06:16:08 +0000978
Jack Jansen0ae32202003-04-09 13:25:43 +0000979 if mode == 'dump':
980 db.dump(sys.stdout)
981 elif mode =='list':
982 if not args:
983 args = db.listnames()
984 print "%-20.20s\t%s" % ("Package", "Description")
985 print
986 for pkgname in args:
987 pkg = db.find(pkgname)
988 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000989 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000990 pkgname = pkg.fullname()
991 else:
992 description = 'Error: no such package'
993 print "%-20.20s\t%s" % (pkgname, description)
994 if verbose:
995 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000996 try:
997 print "\tDownload URL:\t", pkg.downloadURL()
998 except KeyError:
999 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +00001000 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001001 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +00001002 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +00001003 elif mode =='status':
1004 if not args:
1005 args = db.listnames()
1006 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
1007 print
1008 for pkgname in args:
1009 pkg = db.find(pkgname)
1010 if pkg:
1011 status, msg = pkg.installed()
1012 pkgname = pkg.fullname()
1013 else:
1014 status = 'error'
1015 msg = 'No such package'
1016 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
1017 if verbose and status == "no":
1018 prereq = pkg.prerequisites()
1019 for pkg, msg in prereq:
1020 if not pkg:
1021 pkg = ''
1022 else:
1023 pkg = pkg.fullname()
1024 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
1025 elif mode == 'install':
1026 if not args:
1027 print 'Please specify packages to install'
1028 sys.exit(1)
1029 inst = PimpInstaller(db)
1030 for pkgname in args:
1031 pkg = db.find(pkgname)
1032 if not pkg:
1033 print '%s: No such package' % pkgname
1034 continue
1035 list, messages = inst.prepareInstall(pkg, force)
1036 if messages and not force:
1037 print "%s: Not installed:" % pkgname
1038 for m in messages:
1039 print "\t", m
1040 else:
1041 if verbose:
1042 output = sys.stdout
1043 else:
1044 output = None
1045 messages = inst.install(list, output)
1046 if messages:
1047 print "%s: Not installed:" % pkgname
1048 for m in messages:
1049 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +00001050
1051def main():
Jack Jansen0ae32202003-04-09 13:25:43 +00001052 """Minimal commandline tool to drive pimp."""
Tim Peters182b5ac2004-07-18 06:16:08 +00001053
Jack Jansen0ae32202003-04-09 13:25:43 +00001054 import getopt
1055 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001056 print "Usage: pimp [options] -s [package ...] List installed status"
1057 print " pimp [options] -l [package ...] Show package information"
1058 print " pimp [options] -i package ... Install packages"
1059 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +00001060 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +00001061 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001062 print " -v Verbose"
1063 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +00001064 print " -D dir Set destination directory"
1065 print " (default: %s)" % DEFAULT_INSTALLDIR
1066 print " -u url URL for database"
Jack Jansen0ae32202003-04-09 13:25:43 +00001067 sys.exit(1)
Tim Peters182b5ac2004-07-18 06:16:08 +00001068
Jack Jansen989ddc02004-03-11 23:03:59 +00001069 class _Watcher:
1070 def update(self, msg):
1071 sys.stderr.write(msg + '\r')
1072 return 1
Tim Peters182b5ac2004-07-18 06:16:08 +00001073
Jack Jansen0ae32202003-04-09 13:25:43 +00001074 try:
Jack Jansenb789a062003-05-28 18:56:30 +00001075 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
1076 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +00001077 _help()
1078 if not opts and not args:
1079 _help()
1080 mode = None
1081 force = 0
1082 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001083 prefargs = {}
Jack Jansen989ddc02004-03-11 23:03:59 +00001084 watcher = None
Jack Jansen0ae32202003-04-09 13:25:43 +00001085 for o, a in opts:
1086 if o == '-s':
1087 if mode:
1088 _help()
1089 mode = 'status'
1090 if o == '-l':
1091 if mode:
1092 _help()
1093 mode = 'list'
1094 if o == '-d':
1095 if mode:
1096 _help()
1097 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001098 if o == '-V':
1099 if mode:
1100 _help()
1101 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001102 if o == '-i':
1103 mode = 'install'
1104 if o == '-f':
1105 force = 1
1106 if o == '-v':
1107 verbose = 1
Jack Jansen989ddc02004-03-11 23:03:59 +00001108 watcher = _Watcher()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001109 if o == '-D':
1110 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001111 if o == '-u':
1112 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001113 if not mode:
1114 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001115 if mode == 'version':
1116 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1117 else:
Jack Jansen989ddc02004-03-11 23:03:59 +00001118 _run(mode, verbose, force, args, prefargs, watcher)
Jack Jansenb789a062003-05-28 18:56:30 +00001119
1120# Finally, try to update ourselves to a newer version.
1121# If the end-user updates pimp through pimp the new version
1122# will be called pimp_update and live in site-packages
1123# or somewhere similar
1124if __name__ != 'pimp_update':
1125 try:
1126 import pimp_update
1127 except ImportError:
1128 pass
1129 else:
1130 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1131 import warnings
1132 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1133 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1134 else:
1135 from pimp_update import *
Tim Peters182b5ac2004-07-18 06:16:08 +00001136
Jack Jansen95839b82003-02-09 23:10:20 +00001137if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001138 main()