blob: 625e38871641106fd8d0e4c71255f032dde002b5 [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 Jansen702af672004-12-28 21:33:27 +000031 "getDefaultDatabase", "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 Jansenaf304a62005-01-03 15:44:18 +000039PIMP_VERSION="0.5"
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 Jansenaf304a62005-01-03 15:44:18 +000044# installer: something that can be opened
45DEFAULT_FLAVORORDER=['source', 'binary', 'installer']
Jack Jansen95839b82003-02-09 23:10:20 +000046DEFAULT_DOWNLOADDIR='/tmp'
47DEFAULT_BUILDDIR='/tmp'
Jack Jansene71b9f82003-02-12 16:37:00 +000048DEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
Jack Jansenafd63b92004-02-28 22:34:02 +000049DEFAULT_PIMPDATABASE_FMT="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist"
Jack Jansen95839b82003-02-09 23:10:20 +000050
Jack Jansen192bd962004-02-28 23:18:43 +000051def getDefaultDatabase(experimental=False):
Jack Jansen989ddc02004-03-11 23:03:59 +000052 if experimental:
53 status = "exp"
54 else:
55 status = "prod"
Tim Peters182b5ac2004-07-18 06:16:08 +000056
Jack Jansen989ddc02004-03-11 23:03:59 +000057 major, minor, micro, state, extra = sys.version_info
58 pyvers = '%d.%d' % (major, minor)
Jack Jansen702af672004-12-28 21:33:27 +000059 if micro == 0 and state != 'final':
Jack Jansen989ddc02004-03-11 23:03:59 +000060 pyvers = pyvers + '%s%d' % (state, extra)
Tim Peters182b5ac2004-07-18 06:16:08 +000061
Jack Jansen989ddc02004-03-11 23:03:59 +000062 longplatform = distutils.util.get_platform()
63 osname, release, machine = longplatform.split('-')
64 # For some platforms we may want to differentiate between
65 # installation types
66 if osname == 'darwin':
67 if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'):
68 osname = 'darwin_apple'
69 elif sys.prefix.startswith('/Library/Frameworks/Python.framework'):
70 osname = 'darwin_macpython'
71 # Otherwise we don't know...
72 # Now we try various URLs by playing with the release string.
73 # We remove numbers off the end until we find a match.
74 rel = release
75 while True:
76 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine)
77 try:
78 urllib2.urlopen(url)
79 except urllib2.HTTPError, arg:
80 pass
81 else:
82 break
83 if not rel:
84 # We're out of version numbers to try. Use the
85 # full release number, this will give a reasonable
86 # error message later
87 url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine)
88 break
89 idx = rel.rfind('.')
90 if idx < 0:
91 rel = ''
92 else:
93 rel = rel[:idx]
94 return url
Jack Jansen192bd962004-02-28 23:18:43 +000095
Jack Jansen6fde1ce2003-04-15 14:43:05 +000096def _cmd(output, dir, *cmditems):
97 """Internal routine to run a shell command in a given directory."""
Tim Peters182b5ac2004-07-18 06:16:08 +000098
Jack Jansen6fde1ce2003-04-15 14:43:05 +000099 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
100 if output:
101 output.write("+ %s\n" % cmd)
102 if NO_EXECUTE:
103 return 0
104 child = popen2.Popen4(cmd)
105 child.tochild.close()
106 while 1:
107 line = child.fromchild.readline()
108 if not line:
109 break
110 if output:
111 output.write(line)
112 return child.wait()
113
Jack Jansen989ddc02004-03-11 23:03:59 +0000114class PimpDownloader:
115 """Abstract base class - Downloader for archives"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000116
Jack Jansen989ddc02004-03-11 23:03:59 +0000117 def __init__(self, argument,
118 dir="",
119 watcher=None):
120 self.argument = argument
121 self._dir = dir
122 self._watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000123
Jack Jansen989ddc02004-03-11 23:03:59 +0000124 def download(self, url, filename, output=None):
125 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000126
Jack Jansen989ddc02004-03-11 23:03:59 +0000127 def update(self, str):
128 if self._watcher:
129 return self._watcher.update(str)
130 return True
Tim Peters182b5ac2004-07-18 06:16:08 +0000131
Jack Jansen989ddc02004-03-11 23:03:59 +0000132class PimpCurlDownloader(PimpDownloader):
133
134 def download(self, url, filename, output=None):
135 self.update("Downloading %s..." % url)
136 exitstatus = _cmd(output, self._dir,
137 "curl",
138 "--output", filename,
139 url)
140 self.update("Downloading %s: finished" % url)
141 return (not exitstatus)
Tim Peters182b5ac2004-07-18 06:16:08 +0000142
Jack Jansen989ddc02004-03-11 23:03:59 +0000143class PimpUrllibDownloader(PimpDownloader):
144
145 def download(self, url, filename, output=None):
146 output = open(filename, 'wb')
147 self.update("Downloading %s: opening connection" % url)
148 keepgoing = True
149 download = urllib2.urlopen(url)
150 if download.headers.has_key("content-length"):
151 length = long(download.headers['content-length'])
152 else:
153 length = -1
Tim Peters182b5ac2004-07-18 06:16:08 +0000154
Jack Jansen989ddc02004-03-11 23:03:59 +0000155 data = download.read(4096) #read 4K at a time
156 dlsize = 0
157 lasttime = 0
158 while keepgoing:
159 dlsize = dlsize + len(data)
Tim Peters182b5ac2004-07-18 06:16:08 +0000160 if len(data) == 0:
Jack Jansen989ddc02004-03-11 23:03:59 +0000161 #this is our exit condition
162 break
163 output.write(data)
164 if int(time.time()) != lasttime:
165 # Update at most once per second
166 lasttime = int(time.time())
167 if length == -1:
168 keepgoing = self.update("Downloading %s: %d bytes..." % (url, dlsize))
169 else:
170 keepgoing = self.update("Downloading %s: %d%% (%d bytes)..." % (url, int(100.0*dlsize/length), dlsize))
171 data = download.read(4096)
172 if keepgoing:
173 self.update("Downloading %s: finished" % url)
174 return keepgoing
Tim Peters182b5ac2004-07-18 06:16:08 +0000175
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000176class PimpUnpacker:
177 """Abstract base class - Unpacker for archives"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000178
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000179 _can_rename = False
Tim Peters182b5ac2004-07-18 06:16:08 +0000180
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000181 def __init__(self, argument,
182 dir="",
Jack Jansen989ddc02004-03-11 23:03:59 +0000183 renames=[],
184 watcher=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000185 self.argument = argument
186 if renames and not self._can_rename:
187 raise RuntimeError, "This unpacker cannot rename files"
188 self._dir = dir
189 self._renames = renames
Jack Jansen989ddc02004-03-11 23:03:59 +0000190 self._watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000191
Jack Jansen5da131b2003-06-01 20:57:12 +0000192 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000193 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000194
Jack Jansen989ddc02004-03-11 23:03:59 +0000195 def update(self, str):
196 if self._watcher:
197 return self._watcher.update(str)
198 return True
Tim Peters182b5ac2004-07-18 06:16:08 +0000199
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000200class PimpCommandUnpacker(PimpUnpacker):
201 """Unpack archives by calling a Unix utility"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000202
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000203 _can_rename = False
Tim Peters182b5ac2004-07-18 06:16:08 +0000204
Jack Jansen5da131b2003-06-01 20:57:12 +0000205 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000206 cmd = self.argument % archive
207 if _cmd(output, self._dir, cmd):
208 return "unpack command failed"
Tim Peters182b5ac2004-07-18 06:16:08 +0000209
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000210class PimpTarUnpacker(PimpUnpacker):
211 """Unpack tarfiles using the builtin tarfile module"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000212
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000213 _can_rename = True
Tim Peters182b5ac2004-07-18 06:16:08 +0000214
Jack Jansen5da131b2003-06-01 20:57:12 +0000215 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000216 tf = tarfile.open(archive, "r")
217 members = tf.getmembers()
218 skip = []
219 if self._renames:
220 for member in members:
221 for oldprefix, newprefix in self._renames:
222 if oldprefix[:len(self._dir)] == self._dir:
223 oldprefix2 = oldprefix[len(self._dir):]
224 else:
225 oldprefix2 = None
226 if member.name[:len(oldprefix)] == oldprefix:
227 if newprefix is None:
228 skip.append(member)
229 #print 'SKIP', member.name
230 else:
231 member.name = newprefix + member.name[len(oldprefix):]
232 print ' ', member.name
233 break
234 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
235 if newprefix is None:
236 skip.append(member)
237 #print 'SKIP', member.name
238 else:
239 member.name = newprefix + member.name[len(oldprefix2):]
240 #print ' ', member.name
241 break
242 else:
243 skip.append(member)
244 #print '????', member.name
245 for member in members:
246 if member in skip:
Jack Jansen989ddc02004-03-11 23:03:59 +0000247 self.update("Skipping %s" % member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000248 continue
Jack Jansen989ddc02004-03-11 23:03:59 +0000249 self.update("Extracting %s" % member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000250 tf.extract(member, self._dir)
251 if skip:
252 names = [member.name for member in skip if member.name[-1] != '/']
Jack Jansen5da131b2003-06-01 20:57:12 +0000253 if package:
254 names = package.filterExpectedSkips(names)
Jack Jansen6432f782003-04-22 13:56:19 +0000255 if names:
Jack Jansen705553a2003-05-06 12:44:00 +0000256 return "Not all files were unpacked: %s" % " ".join(names)
Tim Peters182b5ac2004-07-18 06:16:08 +0000257
Jack Jansen95839b82003-02-09 23:10:20 +0000258ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000259 (".tar.Z", PimpTarUnpacker, None),
260 (".taz", PimpTarUnpacker, None),
261 (".tar.gz", PimpTarUnpacker, None),
262 (".tgz", PimpTarUnpacker, None),
263 (".tar.bz", PimpTarUnpacker, None),
264 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000265]
266
267class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000268 """Container for per-user preferences, such as the database to use
269 and where to install packages."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000270
271 def __init__(self,
Jack Jansen0ae32202003-04-09 13:25:43 +0000272 flavorOrder=None,
273 downloadDir=None,
274 buildDir=None,
275 installDir=None,
276 pimpDatabase=None):
277 if not flavorOrder:
278 flavorOrder = DEFAULT_FLAVORORDER
279 if not downloadDir:
280 downloadDir = DEFAULT_DOWNLOADDIR
281 if not buildDir:
282 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000283 if not pimpDatabase:
Jack Jansen192bd962004-02-28 23:18:43 +0000284 pimpDatabase = getDefaultDatabase()
Jack Jansen20fa6752003-04-16 12:15:34 +0000285 self.setInstallDir(installDir)
286 self.flavorOrder = flavorOrder
287 self.downloadDir = downloadDir
288 self.buildDir = buildDir
289 self.pimpDatabase = pimpDatabase
Jack Jansen989ddc02004-03-11 23:03:59 +0000290 self.watcher = None
Tim Peters182b5ac2004-07-18 06:16:08 +0000291
Jack Jansen989ddc02004-03-11 23:03:59 +0000292 def setWatcher(self, watcher):
293 self.watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000294
Jack Jansen20fa6752003-04-16 12:15:34 +0000295 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000296 if installDir:
297 # Installing to non-standard location.
298 self.installLocations = [
299 ('--install-lib', installDir),
300 ('--install-headers', None),
301 ('--install-scripts', None),
302 ('--install-data', None)]
303 else:
304 installDir = DEFAULT_INSTALLDIR
305 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000306 self.installDir = installDir
Tim Peters182b5ac2004-07-18 06:16:08 +0000307
Jack Jansen5da131b2003-06-01 20:57:12 +0000308 def isUserInstall(self):
309 return self.installDir != DEFAULT_INSTALLDIR
Jack Jansen20fa6752003-04-16 12:15:34 +0000310
Jack Jansen0ae32202003-04-09 13:25:43 +0000311 def check(self):
312 """Check that the preferences make sense: directories exist and are
313 writable, the install directory is on sys.path, etc."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000314
Jack Jansen0ae32202003-04-09 13:25:43 +0000315 rv = ""
316 RWX_OK = os.R_OK|os.W_OK|os.X_OK
317 if not os.path.exists(self.downloadDir):
318 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
319 elif not os.access(self.downloadDir, RWX_OK):
320 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
321 if not os.path.exists(self.buildDir):
322 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
323 elif not os.access(self.buildDir, RWX_OK):
324 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
325 if not os.path.exists(self.installDir):
326 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
327 elif not os.access(self.installDir, RWX_OK):
328 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
329 else:
330 installDir = os.path.realpath(self.installDir)
331 for p in sys.path:
332 try:
333 realpath = os.path.realpath(p)
334 except:
335 pass
336 if installDir == realpath:
337 break
338 else:
339 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000340 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000341
Jack Jansen0ae32202003-04-09 13:25:43 +0000342 def compareFlavors(self, left, right):
343 """Compare two flavor strings. This is part of your preferences
344 because whether the user prefers installing from source or binary is."""
345 if left in self.flavorOrder:
346 if right in self.flavorOrder:
347 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
348 return -1
349 if right in self.flavorOrder:
350 return 1
351 return cmp(left, right)
Tim Peters182b5ac2004-07-18 06:16:08 +0000352
Jack Jansen95839b82003-02-09 23:10:20 +0000353class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000354 """Class representing a pimp database. It can actually contain
355 information from multiple databases through inclusion, but the
356 toplevel database is considered the master, as its maintainer is
357 "responsible" for the contents."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000358
Jack Jansen0ae32202003-04-09 13:25:43 +0000359 def __init__(self, prefs):
360 self._packages = []
361 self.preferences = prefs
Jack Jansen118e1272004-03-13 23:50:48 +0000362 self._url = ""
Jack Jansen0ae32202003-04-09 13:25:43 +0000363 self._urllist = []
364 self._version = ""
365 self._maintainer = ""
366 self._description = ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000367
Jack Jansen118e1272004-03-13 23:50:48 +0000368 # Accessor functions
369 def url(self): return self._url
370 def version(self): return self._version
371 def maintainer(self): return self._maintainer
372 def description(self): return self._description
Tim Peters182b5ac2004-07-18 06:16:08 +0000373
Jack Jansen0ae32202003-04-09 13:25:43 +0000374 def close(self):
375 """Clean up"""
376 self._packages = []
377 self.preferences = None
Tim Peters182b5ac2004-07-18 06:16:08 +0000378
Jack Jansen0ae32202003-04-09 13:25:43 +0000379 def appendURL(self, url, included=0):
380 """Append packages from the database with the given URL.
381 Only the first database should specify included=0, so the
382 global information (maintainer, description) get stored."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000383
Jack Jansen0ae32202003-04-09 13:25:43 +0000384 if url in self._urllist:
385 return
386 self._urllist.append(url)
387 fp = urllib2.urlopen(url).fp
Jack Jansenc32cec12004-03-13 23:32:47 +0000388 plistdata = plistlib.Plist.fromFile(fp)
Jack Jansen0ae32202003-04-09 13:25:43 +0000389 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000390 if included:
Jack Jansenc32cec12004-03-13 23:32:47 +0000391 version = plistdata.get('Version')
Jack Jansenb789a062003-05-28 18:56:30 +0000392 if version and version > self._version:
393 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
394 (url, version))
395 else:
Jack Jansenc32cec12004-03-13 23:32:47 +0000396 self._version = plistdata.get('Version')
Jack Jansenb789a062003-05-28 18:56:30 +0000397 if not self._version:
398 sys.stderr.write("Warning: database has no Version information\n")
399 elif self._version > PIMP_VERSION:
Tim Peters182b5ac2004-07-18 06:16:08 +0000400 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000401 % (self._version, PIMP_VERSION))
Jack Jansenc32cec12004-03-13 23:32:47 +0000402 self._maintainer = plistdata.get('Maintainer', '')
403 self._description = plistdata.get('Description', '').strip()
Jack Jansen118e1272004-03-13 23:50:48 +0000404 self._url = url
Jack Jansenc32cec12004-03-13 23:32:47 +0000405 self._appendPackages(plistdata['Packages'])
406 others = plistdata.get('Include', [])
Jack Jansen0ae32202003-04-09 13:25:43 +0000407 for url in others:
408 self.appendURL(url, included=1)
Tim Peters182b5ac2004-07-18 06:16:08 +0000409
Jack Jansen0ae32202003-04-09 13:25:43 +0000410 def _appendPackages(self, packages):
411 """Given a list of dictionaries containing package
412 descriptions create the PimpPackage objects and append them
413 to our internal storage."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000414
Jack Jansen0ae32202003-04-09 13:25:43 +0000415 for p in packages:
416 p = dict(p)
417 flavor = p.get('Flavor')
418 if flavor == 'source':
419 pkg = PimpPackage_source(self, p)
420 elif flavor == 'binary':
421 pkg = PimpPackage_binary(self, p)
Jack Jansenaf304a62005-01-03 15:44:18 +0000422 elif flavor == 'installer':
423 pkg = PimpPackage_installer(self, p)
424 elif flavor == 'hidden':
425 pkg = PimpPackage_installer(self, p)
Jack Jansen0ae32202003-04-09 13:25:43 +0000426 else:
427 pkg = PimpPackage(self, dict(p))
428 self._packages.append(pkg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000429
Jack Jansen0ae32202003-04-09 13:25:43 +0000430 def list(self):
431 """Return a list of all PimpPackage objects in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000432
Jack Jansen0ae32202003-04-09 13:25:43 +0000433 return self._packages
Tim Peters182b5ac2004-07-18 06:16:08 +0000434
Jack Jansen0ae32202003-04-09 13:25:43 +0000435 def listnames(self):
436 """Return a list of names of all packages in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000437
Jack Jansen0ae32202003-04-09 13:25:43 +0000438 rv = []
439 for pkg in self._packages:
440 rv.append(pkg.fullname())
441 rv.sort()
442 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000443
Jack Jansen0ae32202003-04-09 13:25:43 +0000444 def dump(self, pathOrFile):
445 """Dump the contents of the database to an XML .plist file.
Tim Peters182b5ac2004-07-18 06:16:08 +0000446
Jack Jansen0ae32202003-04-09 13:25:43 +0000447 The file can be passed as either a file object or a pathname.
448 All data, including included databases, is dumped."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000449
Jack Jansen0ae32202003-04-09 13:25:43 +0000450 packages = []
451 for pkg in self._packages:
452 packages.append(pkg.dump())
Jack Jansenc32cec12004-03-13 23:32:47 +0000453 plistdata = {
Jack Jansen0ae32202003-04-09 13:25:43 +0000454 'Version': self._version,
455 'Maintainer': self._maintainer,
456 'Description': self._description,
457 'Packages': packages
458 }
Jack Jansenc32cec12004-03-13 23:32:47 +0000459 plist = plistlib.Plist(**plistdata)
Jack Jansen0ae32202003-04-09 13:25:43 +0000460 plist.write(pathOrFile)
Tim Peters182b5ac2004-07-18 06:16:08 +0000461
Jack Jansen0ae32202003-04-09 13:25:43 +0000462 def find(self, ident):
463 """Find a package. The package can be specified by name
464 or as a dictionary with name, version and flavor entries.
Tim Peters182b5ac2004-07-18 06:16:08 +0000465
Jack Jansen0ae32202003-04-09 13:25:43 +0000466 Only name is obligatory. If there are multiple matches the
467 best one (higher version number, flavors ordered according to
468 users' preference) is returned."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000469
Jack Jansen0ae32202003-04-09 13:25:43 +0000470 if type(ident) == str:
471 # Remove ( and ) for pseudo-packages
472 if ident[0] == '(' and ident[-1] == ')':
473 ident = ident[1:-1]
474 # Split into name-version-flavor
475 fields = ident.split('-')
476 if len(fields) < 1 or len(fields) > 3:
477 return None
478 name = fields[0]
479 if len(fields) > 1:
480 version = fields[1]
481 else:
482 version = None
483 if len(fields) > 2:
484 flavor = fields[2]
485 else:
486 flavor = None
487 else:
488 name = ident['Name']
489 version = ident.get('Version')
490 flavor = ident.get('Flavor')
491 found = None
492 for p in self._packages:
493 if name == p.name() and \
494 (not version or version == p.version()) and \
495 (not flavor or flavor == p.flavor()):
496 if not found or found < p:
497 found = p
498 return found
Tim Peters182b5ac2004-07-18 06:16:08 +0000499
Jack Jansene7b33db2003-02-11 22:40:59 +0000500ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000501 "Name",
502 "Version",
503 "Flavor",
504 "Description",
505 "Home-page",
506 "Download-URL",
507 "Install-test",
508 "Install-command",
509 "Pre-install-command",
510 "Post-install-command",
511 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000512 "MD5Sum",
513 "User-install-skips",
514 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000515]
516
Jack Jansen95839b82003-02-09 23:10:20 +0000517class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000518 """Class representing a single package."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000519
Jack Jansenc32cec12004-03-13 23:32:47 +0000520 def __init__(self, db, plistdata):
Jack Jansen0ae32202003-04-09 13:25:43 +0000521 self._db = db
Jack Jansenc32cec12004-03-13 23:32:47 +0000522 name = plistdata["Name"]
523 for k in plistdata.keys():
Jack Jansen0ae32202003-04-09 13:25:43 +0000524 if not k in ALLOWED_KEYS:
525 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
Jack Jansenc32cec12004-03-13 23:32:47 +0000526 self._dict = plistdata
Tim Peters182b5ac2004-07-18 06:16:08 +0000527
Jack Jansen0ae32202003-04-09 13:25:43 +0000528 def __getitem__(self, key):
529 return self._dict[key]
Tim Peters182b5ac2004-07-18 06:16:08 +0000530
Jack Jansen0ae32202003-04-09 13:25:43 +0000531 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000532 def version(self): return self._dict.get('Version')
533 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000534 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000535 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000536 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000537 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000538 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Tim Peters182b5ac2004-07-18 06:16:08 +0000539
Jack Jansen0ae32202003-04-09 13:25:43 +0000540 def fullname(self):
541 """Return the full name "name-version-flavor" of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000542
Jack Jansen0ae32202003-04-09 13:25:43 +0000543 If the package is a pseudo-package, something that cannot be
544 installed through pimp, return the name in (parentheses)."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000545
Jack Jansen0ae32202003-04-09 13:25:43 +0000546 rv = self._dict['Name']
547 if self._dict.has_key('Version'):
548 rv = rv + '-%s' % self._dict['Version']
549 if self._dict.has_key('Flavor'):
550 rv = rv + '-%s' % self._dict['Flavor']
Jack Jansenaf304a62005-01-03 15:44:18 +0000551 if self._dict.get('Flavor') == 'hidden':
Jack Jansen0ae32202003-04-09 13:25:43 +0000552 # Pseudo-package, show in parentheses
553 rv = '(%s)' % rv
554 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000555
Jack Jansen0ae32202003-04-09 13:25:43 +0000556 def dump(self):
557 """Return a dict object containing the information on the package."""
558 return self._dict
Tim Peters182b5ac2004-07-18 06:16:08 +0000559
Jack Jansen0ae32202003-04-09 13:25:43 +0000560 def __cmp__(self, other):
561 """Compare two packages, where the "better" package sorts lower."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000562
Jack Jansen0ae32202003-04-09 13:25:43 +0000563 if not isinstance(other, PimpPackage):
564 return cmp(id(self), id(other))
565 if self.name() != other.name():
566 return cmp(self.name(), other.name())
567 if self.version() != other.version():
568 return -cmp(self.version(), other.version())
569 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Tim Peters182b5ac2004-07-18 06:16:08 +0000570
Jack Jansen0ae32202003-04-09 13:25:43 +0000571 def installed(self):
572 """Test wheter the package is installed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000573
Jack Jansen0ae32202003-04-09 13:25:43 +0000574 Returns two values: a status indicator which is one of
575 "yes", "no", "old" (an older version is installed) or "bad"
576 (something went wrong during the install test) and a human
577 readable string which may contain more details."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000578
Jack Jansen0ae32202003-04-09 13:25:43 +0000579 namespace = {
580 "NotInstalled": _scriptExc_NotInstalled,
581 "OldInstalled": _scriptExc_OldInstalled,
582 "BadInstalled": _scriptExc_BadInstalled,
583 "os": os,
584 "sys": sys,
585 }
586 installTest = self._dict['Install-test'].strip() + '\n'
587 try:
588 exec installTest in namespace
589 except ImportError, arg:
590 return "no", str(arg)
591 except _scriptExc_NotInstalled, arg:
592 return "no", str(arg)
593 except _scriptExc_OldInstalled, arg:
594 return "old", str(arg)
595 except _scriptExc_BadInstalled, arg:
596 return "bad", str(arg)
597 except:
598 sys.stderr.write("-------------------------------------\n")
599 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
600 sys.stderr.write("---- source:\n")
601 sys.stderr.write(installTest)
602 sys.stderr.write("---- exception:\n")
603 import traceback
604 traceback.print_exc(file=sys.stderr)
605 if self._db._maintainer:
606 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
607 sys.stderr.write("-------------------------------------\n")
608 return "bad", "Package install test got exception"
609 return "yes", ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000610
Jack Jansen0ae32202003-04-09 13:25:43 +0000611 def prerequisites(self):
612 """Return a list of prerequisites for this package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000613
Jack Jansen0ae32202003-04-09 13:25:43 +0000614 The list contains 2-tuples, of which the first item is either
615 a PimpPackage object or None, and the second is a descriptive
616 string. The first item can be None if this package depends on
617 something that isn't pimp-installable, in which case the descriptive
618 string should tell the user what to do."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000619
Jack Jansen0ae32202003-04-09 13:25:43 +0000620 rv = []
621 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000622 # For pseudo-packages that are already installed we don't
623 # return an error message
624 status, _ = self.installed()
625 if status == "yes":
626 return []
Tim Peters182b5ac2004-07-18 06:16:08 +0000627 return [(None,
Jack Jansenaf304a62005-01-03 15:44:18 +0000628 "Package %s cannot be installed automatically, see the description" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000629 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000630 if self.systemwideOnly() and self._db.preferences.isUserInstall():
631 return [(None,
Jack Jansenaf304a62005-01-03 15:44:18 +0000632 "Package %s can only be installed system-wide" %
Jack Jansen5da131b2003-06-01 20:57:12 +0000633 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000634 if not self._dict.get('Prerequisites'):
635 return []
636 for item in self._dict['Prerequisites']:
637 if type(item) == str:
638 pkg = None
639 descr = str(item)
640 else:
641 name = item['Name']
642 if item.has_key('Version'):
643 name = name + '-' + item['Version']
644 if item.has_key('Flavor'):
645 name = name + '-' + item['Flavor']
646 pkg = self._db.find(name)
647 if not pkg:
648 descr = "Requires unknown %s"%name
649 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000650 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000651 rv.append((pkg, descr))
652 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000653
654
Jack Jansen0ae32202003-04-09 13:25:43 +0000655 def downloadPackageOnly(self, output=None):
656 """Download a single package, if needed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000657
Jack Jansen0ae32202003-04-09 13:25:43 +0000658 An MD5 signature is used to determine whether download is needed,
659 and to test that we actually downloaded what we expected.
660 If output is given it is a file-like object that will receive a log
661 of what happens.
Tim Peters182b5ac2004-07-18 06:16:08 +0000662
Jack Jansen0ae32202003-04-09 13:25:43 +0000663 If anything unforeseen happened the method returns an error message
664 string.
665 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000666
Jack Jansen0ae32202003-04-09 13:25:43 +0000667 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
668 path = urllib.url2pathname(path)
669 filename = os.path.split(path)[1]
Tim Peters182b5ac2004-07-18 06:16:08 +0000670 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
Jack Jansen0ae32202003-04-09 13:25:43 +0000671 if not self._archiveOK():
672 if scheme == 'manual':
673 return "Please download package manually and save as %s" % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000674 downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000675 watcher=self._db.preferences.watcher)
676 if not downloader.download(self._dict['Download-URL'],
677 self.archiveFilename, output):
Jack Jansen0ae32202003-04-09 13:25:43 +0000678 return "download command failed"
679 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
680 return "archive not found after download"
681 if not self._archiveOK():
682 return "archive does not have correct MD5 checksum"
Tim Peters182b5ac2004-07-18 06:16:08 +0000683
Jack Jansen0ae32202003-04-09 13:25:43 +0000684 def _archiveOK(self):
685 """Test an archive. It should exist and the MD5 checksum should be correct."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000686
Jack Jansen0ae32202003-04-09 13:25:43 +0000687 if not os.path.exists(self.archiveFilename):
688 return 0
689 if not self._dict.get('MD5Sum'):
690 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
691 return 1
692 data = open(self.archiveFilename, 'rb').read()
693 checksum = md5.new(data).hexdigest()
694 return checksum == self._dict['MD5Sum']
Tim Peters182b5ac2004-07-18 06:16:08 +0000695
Jack Jansen0ae32202003-04-09 13:25:43 +0000696 def unpackPackageOnly(self, output=None):
697 """Unpack a downloaded package archive."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000698
Jack Jansen0ae32202003-04-09 13:25:43 +0000699 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000700 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000701 if filename[-len(ext):] == ext:
702 break
703 else:
704 return "unknown extension for archive file: %s" % filename
705 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000706 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000707 watcher=self._db.preferences.watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000708 rv = unpacker.unpack(self.archiveFilename, output=output)
709 if rv:
710 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000711
Jack Jansen0ae32202003-04-09 13:25:43 +0000712 def installPackageOnly(self, output=None):
713 """Default install method, to be overridden by subclasses"""
714 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
715 % (self.fullname(), self._dict.get(flavor, ""))
Tim Peters182b5ac2004-07-18 06:16:08 +0000716
Jack Jansen0ae32202003-04-09 13:25:43 +0000717 def installSinglePackage(self, output=None):
718 """Download, unpack and install a single package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000719
Jack Jansen0ae32202003-04-09 13:25:43 +0000720 If output is given it should be a file-like object and it
721 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000722
Jack Jansen749f4812003-07-21 20:47:11 +0000723 if not self._dict.get('Download-URL'):
724 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000725 msg = self.downloadPackageOnly(output)
726 if msg:
727 return "%s: download: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000728
Jack Jansen0ae32202003-04-09 13:25:43 +0000729 msg = self.unpackPackageOnly(output)
730 if msg:
731 return "%s: unpack: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000732
Jack Jansen0ae32202003-04-09 13:25:43 +0000733 return self.installPackageOnly(output)
Tim Peters182b5ac2004-07-18 06:16:08 +0000734
Jack Jansen0ae32202003-04-09 13:25:43 +0000735 def beforeInstall(self):
736 """Bookkeeping before installation: remember what we have in site-packages"""
737 self._old_contents = os.listdir(self._db.preferences.installDir)
Tim Peters182b5ac2004-07-18 06:16:08 +0000738
Jack Jansen0ae32202003-04-09 13:25:43 +0000739 def afterInstall(self):
740 """Bookkeeping after installation: interpret any new .pth files that have
741 appeared"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000742
Jack Jansen0ae32202003-04-09 13:25:43 +0000743 new_contents = os.listdir(self._db.preferences.installDir)
744 for fn in new_contents:
745 if fn in self._old_contents:
746 continue
747 if fn[-4:] != '.pth':
748 continue
749 fullname = os.path.join(self._db.preferences.installDir, fn)
750 f = open(fullname)
751 for line in f.readlines():
752 if not line:
753 continue
754 if line[0] == '#':
755 continue
756 if line[:6] == 'import':
757 exec line
758 continue
759 if line[-1] == '\n':
760 line = line[:-1]
761 if not os.path.isabs(line):
762 line = os.path.join(self._db.preferences.installDir, line)
763 line = os.path.realpath(line)
764 if not line in sys.path:
Tim Peters182b5ac2004-07-18 06:16:08 +0000765 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000766
Jack Jansen5da131b2003-06-01 20:57:12 +0000767 def filterExpectedSkips(self, names):
768 """Return a list that contains only unpexpected skips"""
769 if not self._db.preferences.isUserInstall():
770 return names
771 expected_skips = self._dict.get('User-install-skips')
772 if not expected_skips:
773 return names
774 newnames = []
775 for name in names:
776 for skip in expected_skips:
777 if name[:len(skip)] == skip:
778 break
779 else:
780 newnames.append(name)
781 return newnames
782
Jack Jansen0dacac42003-02-14 14:11:59 +0000783class PimpPackage_binary(PimpPackage):
784
Jack Jansen0ae32202003-04-09 13:25:43 +0000785 def unpackPackageOnly(self, output=None):
786 """We don't unpack binary packages until installing"""
787 pass
Tim Peters182b5ac2004-07-18 06:16:08 +0000788
Jack Jansen0ae32202003-04-09 13:25:43 +0000789 def installPackageOnly(self, output=None):
790 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000791
Jack Jansen0ae32202003-04-09 13:25:43 +0000792 If output is given it should be a file-like object and it
793 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000794
Jack Jansen0ae32202003-04-09 13:25:43 +0000795 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000796 return "%s: Binary package cannot have Install-command" % self.fullname()
Tim Peters182b5ac2004-07-18 06:16:08 +0000797
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000798 if self._dict.has_key('Pre-install-command'):
Jack Jansenaf304a62005-01-03 15:44:18 +0000799 if _cmd(output, '/tmp', self._dict['Pre-install-command']):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000800 return "pre-install %s: running \"%s\" failed" % \
801 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000802
Jack Jansen0ae32202003-04-09 13:25:43 +0000803 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000804
Jack Jansen0ae32202003-04-09 13:25:43 +0000805 # Install by unpacking
806 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000807 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000808 if filename[-len(ext):] == ext:
809 break
810 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000811 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
812 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000813
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000814 install_renames = []
815 for k, newloc in self._db.preferences.installLocations:
816 if not newloc:
817 continue
818 if k == "--install-lib":
819 oldloc = DEFAULT_INSTALLDIR
820 else:
821 return "%s: Don't know installLocation %s" % (self.fullname(), k)
822 install_renames.append((oldloc, newloc))
Tim Peters182b5ac2004-07-18 06:16:08 +0000823
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000824 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000825 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000826 if rv:
827 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000828
Jack Jansen0ae32202003-04-09 13:25:43 +0000829 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000830
Jack Jansen0ae32202003-04-09 13:25:43 +0000831 if self._dict.has_key('Post-install-command'):
Jack Jansenaf304a62005-01-03 15:44:18 +0000832 if _cmd(output, '/tmp', self._dict['Post-install-command']):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000833 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000834 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000835
Jack Jansen0ae32202003-04-09 13:25:43 +0000836 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000837
838
Jack Jansen0dacac42003-02-14 14:11:59 +0000839class PimpPackage_source(PimpPackage):
840
Jack Jansen0ae32202003-04-09 13:25:43 +0000841 def unpackPackageOnly(self, output=None):
842 """Unpack a source package and check that setup.py exists"""
843 PimpPackage.unpackPackageOnly(self, output)
844 # Test that a setup script has been create
845 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
846 setupname = os.path.join(self._buildDirname, "setup.py")
847 if not os.path.exists(setupname) and not NO_EXECUTE:
848 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000849
Jack Jansen0ae32202003-04-09 13:25:43 +0000850 def installPackageOnly(self, output=None):
851 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000852
Jack Jansen0ae32202003-04-09 13:25:43 +0000853 If output is given it should be a file-like object and it
854 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000855
Jack Jansen0ae32202003-04-09 13:25:43 +0000856 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000857 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000858 return "pre-install %s: running \"%s\" failed" % \
859 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000860
Jack Jansen0ae32202003-04-09 13:25:43 +0000861 self.beforeInstall()
862 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000863 if installcmd and self._install_renames:
864 return "Package has install-command and can only be installed to standard location"
865 # This is the "bit-bucket" for installations: everything we don't
866 # want. After installation we check that it is actually empty
867 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000868 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000869 extra_args = ""
870 for k, v in self._db.preferences.installLocations:
871 if not v:
872 # We don't want these files installed. Send them
873 # to the bit-bucket.
874 if not unwanted_install_dir:
875 unwanted_install_dir = tempfile.mkdtemp()
876 v = unwanted_install_dir
877 extra_args = extra_args + " %s \"%s\"" % (k, v)
878 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
879 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000880 return "install %s: running \"%s\" failed" % \
881 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000882 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
883 unwanted_files = os.listdir(unwanted_install_dir)
884 if unwanted_files:
885 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
886 else:
887 rv = None
888 shutil.rmtree(unwanted_install_dir)
889 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000890
Jack Jansen0ae32202003-04-09 13:25:43 +0000891 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000892
Jack Jansen0ae32202003-04-09 13:25:43 +0000893 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000894 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000895 return "post-install %s: running \"%s\" failed" % \
896 (self.fullname(), self._dict['Post-install-command'])
897 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000898
Jack Jansenaf304a62005-01-03 15:44:18 +0000899class PimpPackage_installer(PimpPackage):
900
901 def unpackPackageOnly(self, output=None):
902 """We don't unpack dmg packages until installing"""
903 pass
904
905 def installPackageOnly(self, output=None):
906 """Install a single source package.
907
908 If output is given it should be a file-like object and it
909 will receive a log of what happened."""
910
911 if self._dict.has_key('Post-install-command'):
912 return "%s: Installer package cannot have Post-install-command" % self.fullname()
913
914 if self._dict.has_key('Pre-install-command'):
915 if _cmd(output, '/tmp', self._dict['Pre-install-command']):
916 return "pre-install %s: running \"%s\" failed" % \
917 (self.fullname(), self._dict['Pre-install-command'])
918
919 self.beforeInstall()
920
921 installcmd = self._dict.get('Install-command')
922 if installcmd:
923 if '%' in installcmd:
924 installcmd = installcmd % self.archiveFilename
925 else:
926 installcmd = 'open \"%s\"' % self.archiveFilename
927 if _cmd(output, "/tmp", installcmd):
928 return '%s: install command failed (use verbose for details)' % self.fullname()
929 return '%s: downloaded and opened. Install manually and restart Package Manager' % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000930
Jack Jansen95839b82003-02-09 23:10:20 +0000931class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000932 """Installer engine: computes dependencies and installs
933 packages in the right order."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000934
Jack Jansen0ae32202003-04-09 13:25:43 +0000935 def __init__(self, db):
936 self._todo = []
937 self._db = db
938 self._curtodo = []
939 self._curmessages = []
Tim Peters182b5ac2004-07-18 06:16:08 +0000940
Jack Jansen0ae32202003-04-09 13:25:43 +0000941 def __contains__(self, package):
942 return package in self._todo
Tim Peters182b5ac2004-07-18 06:16:08 +0000943
Jack Jansen0ae32202003-04-09 13:25:43 +0000944 def _addPackages(self, packages):
945 for package in packages:
946 if not package in self._todo:
Jack Jansen118e1272004-03-13 23:50:48 +0000947 self._todo.append(package)
Tim Peters182b5ac2004-07-18 06:16:08 +0000948
Jack Jansen0ae32202003-04-09 13:25:43 +0000949 def _prepareInstall(self, package, force=0, recursive=1):
950 """Internal routine, recursive engine for prepareInstall.
Tim Peters182b5ac2004-07-18 06:16:08 +0000951
Jack Jansen0ae32202003-04-09 13:25:43 +0000952 Test whether the package is installed and (if not installed
953 or if force==1) prepend it to the temporary todo list and
954 call ourselves recursively on all prerequisites."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000955
Jack Jansen0ae32202003-04-09 13:25:43 +0000956 if not force:
957 status, message = package.installed()
958 if status == "yes":
Tim Peters182b5ac2004-07-18 06:16:08 +0000959 return
Jack Jansen0ae32202003-04-09 13:25:43 +0000960 if package in self._todo or package in self._curtodo:
961 return
962 self._curtodo.insert(0, package)
963 if not recursive:
964 return
965 prereqs = package.prerequisites()
966 for pkg, descr in prereqs:
967 if pkg:
Jack Jansen0576d0a2004-03-13 23:03:38 +0000968 self._prepareInstall(pkg, False, recursive)
Jack Jansen0ae32202003-04-09 13:25:43 +0000969 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000970 self._curmessages.append("Problem with dependency: %s" % descr)
Tim Peters182b5ac2004-07-18 06:16:08 +0000971
Jack Jansen0ae32202003-04-09 13:25:43 +0000972 def prepareInstall(self, package, force=0, recursive=1):
973 """Prepare installation of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000974
Jack Jansen0ae32202003-04-09 13:25:43 +0000975 If the package is already installed and force is false nothing
976 is done. If recursive is true prerequisites are installed first.
Tim Peters182b5ac2004-07-18 06:16:08 +0000977
Jack Jansen0ae32202003-04-09 13:25:43 +0000978 Returns a list of packages (to be passed to install) and a list
979 of messages of any problems encountered.
980 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000981
Jack Jansen0ae32202003-04-09 13:25:43 +0000982 self._curtodo = []
983 self._curmessages = []
984 self._prepareInstall(package, force, recursive)
985 rv = self._curtodo, self._curmessages
986 self._curtodo = []
987 self._curmessages = []
988 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000989
Jack Jansen0ae32202003-04-09 13:25:43 +0000990 def install(self, packages, output):
991 """Install a list of packages."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000992
Jack Jansen0ae32202003-04-09 13:25:43 +0000993 self._addPackages(packages)
994 status = []
995 for pkg in self._todo:
996 msg = pkg.installSinglePackage(output)
997 if msg:
998 status.append(msg)
999 return status
Tim Peters182b5ac2004-07-18 06:16:08 +00001000
1001
1002
Jack Jansen989ddc02004-03-11 23:03:59 +00001003def _run(mode, verbose, force, args, prefargs, watcher):
Jack Jansen0ae32202003-04-09 13:25:43 +00001004 """Engine for the main program"""
Tim Peters182b5ac2004-07-18 06:16:08 +00001005
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001006 prefs = PimpPreferences(**prefargs)
Jack Jansen989ddc02004-03-11 23:03:59 +00001007 if watcher:
1008 prefs.setWatcher(watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001009 rv = prefs.check()
1010 if rv:
1011 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +00001012 db = PimpDatabase(prefs)
1013 db.appendURL(prefs.pimpDatabase)
Tim Peters182b5ac2004-07-18 06:16:08 +00001014
Jack Jansen0ae32202003-04-09 13:25:43 +00001015 if mode == 'dump':
1016 db.dump(sys.stdout)
1017 elif mode =='list':
1018 if not args:
1019 args = db.listnames()
1020 print "%-20.20s\t%s" % ("Package", "Description")
1021 print
1022 for pkgname in args:
1023 pkg = db.find(pkgname)
1024 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001025 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +00001026 pkgname = pkg.fullname()
1027 else:
1028 description = 'Error: no such package'
1029 print "%-20.20s\t%s" % (pkgname, description)
1030 if verbose:
1031 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001032 try:
1033 print "\tDownload URL:\t", pkg.downloadURL()
1034 except KeyError:
1035 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +00001036 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001037 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +00001038 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +00001039 elif mode =='status':
1040 if not args:
1041 args = db.listnames()
1042 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
1043 print
1044 for pkgname in args:
1045 pkg = db.find(pkgname)
1046 if pkg:
1047 status, msg = pkg.installed()
1048 pkgname = pkg.fullname()
1049 else:
1050 status = 'error'
1051 msg = 'No such package'
1052 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
1053 if verbose and status == "no":
1054 prereq = pkg.prerequisites()
1055 for pkg, msg in prereq:
1056 if not pkg:
1057 pkg = ''
1058 else:
1059 pkg = pkg.fullname()
1060 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
1061 elif mode == 'install':
1062 if not args:
1063 print 'Please specify packages to install'
1064 sys.exit(1)
1065 inst = PimpInstaller(db)
1066 for pkgname in args:
1067 pkg = db.find(pkgname)
1068 if not pkg:
1069 print '%s: No such package' % pkgname
1070 continue
1071 list, messages = inst.prepareInstall(pkg, force)
1072 if messages and not force:
1073 print "%s: Not installed:" % pkgname
1074 for m in messages:
1075 print "\t", m
1076 else:
1077 if verbose:
1078 output = sys.stdout
1079 else:
1080 output = None
1081 messages = inst.install(list, output)
1082 if messages:
1083 print "%s: Not installed:" % pkgname
1084 for m in messages:
1085 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +00001086
1087def main():
Jack Jansen0ae32202003-04-09 13:25:43 +00001088 """Minimal commandline tool to drive pimp."""
Tim Peters182b5ac2004-07-18 06:16:08 +00001089
Jack Jansen0ae32202003-04-09 13:25:43 +00001090 import getopt
1091 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001092 print "Usage: pimp [options] -s [package ...] List installed status"
1093 print " pimp [options] -l [package ...] Show package information"
1094 print " pimp [options] -i package ... Install packages"
1095 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +00001096 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +00001097 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001098 print " -v Verbose"
1099 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +00001100 print " -D dir Set destination directory"
1101 print " (default: %s)" % DEFAULT_INSTALLDIR
1102 print " -u url URL for database"
Jack Jansen0ae32202003-04-09 13:25:43 +00001103 sys.exit(1)
Tim Peters182b5ac2004-07-18 06:16:08 +00001104
Jack Jansen989ddc02004-03-11 23:03:59 +00001105 class _Watcher:
1106 def update(self, msg):
1107 sys.stderr.write(msg + '\r')
1108 return 1
Tim Peters182b5ac2004-07-18 06:16:08 +00001109
Jack Jansen0ae32202003-04-09 13:25:43 +00001110 try:
Jack Jansenb789a062003-05-28 18:56:30 +00001111 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
1112 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +00001113 _help()
1114 if not opts and not args:
1115 _help()
1116 mode = None
1117 force = 0
1118 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001119 prefargs = {}
Jack Jansen989ddc02004-03-11 23:03:59 +00001120 watcher = None
Jack Jansen0ae32202003-04-09 13:25:43 +00001121 for o, a in opts:
1122 if o == '-s':
1123 if mode:
1124 _help()
1125 mode = 'status'
1126 if o == '-l':
1127 if mode:
1128 _help()
1129 mode = 'list'
1130 if o == '-d':
1131 if mode:
1132 _help()
1133 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001134 if o == '-V':
1135 if mode:
1136 _help()
1137 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001138 if o == '-i':
1139 mode = 'install'
1140 if o == '-f':
1141 force = 1
1142 if o == '-v':
1143 verbose = 1
Jack Jansen989ddc02004-03-11 23:03:59 +00001144 watcher = _Watcher()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001145 if o == '-D':
1146 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001147 if o == '-u':
1148 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001149 if not mode:
1150 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001151 if mode == 'version':
1152 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1153 else:
Jack Jansen989ddc02004-03-11 23:03:59 +00001154 _run(mode, verbose, force, args, prefargs, watcher)
Jack Jansenb789a062003-05-28 18:56:30 +00001155
1156# Finally, try to update ourselves to a newer version.
1157# If the end-user updates pimp through pimp the new version
1158# will be called pimp_update and live in site-packages
1159# or somewhere similar
1160if __name__ != 'pimp_update':
1161 try:
1162 import pimp_update
1163 except ImportError:
1164 pass
1165 else:
1166 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1167 import warnings
1168 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1169 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1170 else:
1171 from pimp_update import *
Tim Peters182b5ac2004-07-18 06:16:08 +00001172
Jack Jansen95839b82003-02-09 23:10:20 +00001173if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001174 main()