blob: 255b57cceefc71e1a5d71ee4a90974cbe0461ddb [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 Jansen1dd087c2005-01-07 13:43:31 +0000405 self._appendPackages(plistdata['Packages'], url)
Jack Jansenc32cec12004-03-13 23:32:47 +0000406 others = plistdata.get('Include', [])
Jack Jansen1dd087c2005-01-07 13:43:31 +0000407 for o in others:
408 o = urllib.basejoin(url, o)
409 self.appendURL(o, included=1)
Tim Peters182b5ac2004-07-18 06:16:08 +0000410
Jack Jansen1dd087c2005-01-07 13:43:31 +0000411 def _appendPackages(self, packages, url):
Jack Jansen0ae32202003-04-09 13:25:43 +0000412 """Given a list of dictionaries containing package
413 descriptions create the PimpPackage objects and append them
414 to our internal storage."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000415
Jack Jansen0ae32202003-04-09 13:25:43 +0000416 for p in packages:
417 p = dict(p)
Jack Jansen1dd087c2005-01-07 13:43:31 +0000418 if p.has_key('Download-URL'):
419 p['Download-URL'] = urllib.basejoin(url, p['Download-URL'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000420 flavor = p.get('Flavor')
421 if flavor == 'source':
422 pkg = PimpPackage_source(self, p)
423 elif flavor == 'binary':
424 pkg = PimpPackage_binary(self, p)
Jack Jansenaf304a62005-01-03 15:44:18 +0000425 elif flavor == 'installer':
426 pkg = PimpPackage_installer(self, p)
427 elif flavor == 'hidden':
428 pkg = PimpPackage_installer(self, p)
Jack Jansen0ae32202003-04-09 13:25:43 +0000429 else:
430 pkg = PimpPackage(self, dict(p))
431 self._packages.append(pkg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000432
Jack Jansen0ae32202003-04-09 13:25:43 +0000433 def list(self):
434 """Return a list of all PimpPackage objects in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000435
Jack Jansen0ae32202003-04-09 13:25:43 +0000436 return self._packages
Tim Peters182b5ac2004-07-18 06:16:08 +0000437
Jack Jansen0ae32202003-04-09 13:25:43 +0000438 def listnames(self):
439 """Return a list of names of all packages in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000440
Jack Jansen0ae32202003-04-09 13:25:43 +0000441 rv = []
442 for pkg in self._packages:
443 rv.append(pkg.fullname())
444 rv.sort()
445 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000446
Jack Jansen0ae32202003-04-09 13:25:43 +0000447 def dump(self, pathOrFile):
448 """Dump the contents of the database to an XML .plist file.
Tim Peters182b5ac2004-07-18 06:16:08 +0000449
Jack Jansen0ae32202003-04-09 13:25:43 +0000450 The file can be passed as either a file object or a pathname.
451 All data, including included databases, is dumped."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000452
Jack Jansen0ae32202003-04-09 13:25:43 +0000453 packages = []
454 for pkg in self._packages:
455 packages.append(pkg.dump())
Jack Jansenc32cec12004-03-13 23:32:47 +0000456 plistdata = {
Jack Jansen0ae32202003-04-09 13:25:43 +0000457 'Version': self._version,
458 'Maintainer': self._maintainer,
459 'Description': self._description,
460 'Packages': packages
461 }
Jack Jansenc32cec12004-03-13 23:32:47 +0000462 plist = plistlib.Plist(**plistdata)
Jack Jansen0ae32202003-04-09 13:25:43 +0000463 plist.write(pathOrFile)
Tim Peters182b5ac2004-07-18 06:16:08 +0000464
Jack Jansen0ae32202003-04-09 13:25:43 +0000465 def find(self, ident):
466 """Find a package. The package can be specified by name
467 or as a dictionary with name, version and flavor entries.
Tim Peters182b5ac2004-07-18 06:16:08 +0000468
Jack Jansen0ae32202003-04-09 13:25:43 +0000469 Only name is obligatory. If there are multiple matches the
470 best one (higher version number, flavors ordered according to
471 users' preference) is returned."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000472
Jack Jansen0ae32202003-04-09 13:25:43 +0000473 if type(ident) == str:
474 # Remove ( and ) for pseudo-packages
475 if ident[0] == '(' and ident[-1] == ')':
476 ident = ident[1:-1]
477 # Split into name-version-flavor
478 fields = ident.split('-')
479 if len(fields) < 1 or len(fields) > 3:
480 return None
481 name = fields[0]
482 if len(fields) > 1:
483 version = fields[1]
484 else:
485 version = None
486 if len(fields) > 2:
487 flavor = fields[2]
488 else:
489 flavor = None
490 else:
491 name = ident['Name']
492 version = ident.get('Version')
493 flavor = ident.get('Flavor')
494 found = None
495 for p in self._packages:
496 if name == p.name() and \
497 (not version or version == p.version()) and \
498 (not flavor or flavor == p.flavor()):
499 if not found or found < p:
500 found = p
501 return found
Tim Peters182b5ac2004-07-18 06:16:08 +0000502
Jack Jansene7b33db2003-02-11 22:40:59 +0000503ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000504 "Name",
505 "Version",
506 "Flavor",
507 "Description",
508 "Home-page",
509 "Download-URL",
510 "Install-test",
511 "Install-command",
512 "Pre-install-command",
513 "Post-install-command",
514 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000515 "MD5Sum",
516 "User-install-skips",
517 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000518]
519
Jack Jansen95839b82003-02-09 23:10:20 +0000520class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000521 """Class representing a single package."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000522
Jack Jansenc32cec12004-03-13 23:32:47 +0000523 def __init__(self, db, plistdata):
Jack Jansen0ae32202003-04-09 13:25:43 +0000524 self._db = db
Jack Jansenc32cec12004-03-13 23:32:47 +0000525 name = plistdata["Name"]
526 for k in plistdata.keys():
Jack Jansen0ae32202003-04-09 13:25:43 +0000527 if not k in ALLOWED_KEYS:
528 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
Jack Jansenc32cec12004-03-13 23:32:47 +0000529 self._dict = plistdata
Tim Peters182b5ac2004-07-18 06:16:08 +0000530
Jack Jansen0ae32202003-04-09 13:25:43 +0000531 def __getitem__(self, key):
532 return self._dict[key]
Tim Peters182b5ac2004-07-18 06:16:08 +0000533
Jack Jansen0ae32202003-04-09 13:25:43 +0000534 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000535 def version(self): return self._dict.get('Version')
536 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000537 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000538 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000539 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000540 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000541 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Tim Peters182b5ac2004-07-18 06:16:08 +0000542
Jack Jansen0ae32202003-04-09 13:25:43 +0000543 def fullname(self):
544 """Return the full name "name-version-flavor" of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000545
Jack Jansen0ae32202003-04-09 13:25:43 +0000546 If the package is a pseudo-package, something that cannot be
547 installed through pimp, return the name in (parentheses)."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000548
Jack Jansen0ae32202003-04-09 13:25:43 +0000549 rv = self._dict['Name']
550 if self._dict.has_key('Version'):
551 rv = rv + '-%s' % self._dict['Version']
552 if self._dict.has_key('Flavor'):
553 rv = rv + '-%s' % self._dict['Flavor']
Jack Jansenaf304a62005-01-03 15:44:18 +0000554 if self._dict.get('Flavor') == 'hidden':
Jack Jansen0ae32202003-04-09 13:25:43 +0000555 # Pseudo-package, show in parentheses
556 rv = '(%s)' % rv
557 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000558
Jack Jansen0ae32202003-04-09 13:25:43 +0000559 def dump(self):
560 """Return a dict object containing the information on the package."""
561 return self._dict
Tim Peters182b5ac2004-07-18 06:16:08 +0000562
Jack Jansen0ae32202003-04-09 13:25:43 +0000563 def __cmp__(self, other):
564 """Compare two packages, where the "better" package sorts lower."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000565
Jack Jansen0ae32202003-04-09 13:25:43 +0000566 if not isinstance(other, PimpPackage):
567 return cmp(id(self), id(other))
568 if self.name() != other.name():
569 return cmp(self.name(), other.name())
570 if self.version() != other.version():
571 return -cmp(self.version(), other.version())
572 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Tim Peters182b5ac2004-07-18 06:16:08 +0000573
Jack Jansen0ae32202003-04-09 13:25:43 +0000574 def installed(self):
575 """Test wheter the package is installed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000576
Jack Jansen0ae32202003-04-09 13:25:43 +0000577 Returns two values: a status indicator which is one of
578 "yes", "no", "old" (an older version is installed) or "bad"
579 (something went wrong during the install test) and a human
580 readable string which may contain more details."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000581
Jack Jansen0ae32202003-04-09 13:25:43 +0000582 namespace = {
583 "NotInstalled": _scriptExc_NotInstalled,
584 "OldInstalled": _scriptExc_OldInstalled,
585 "BadInstalled": _scriptExc_BadInstalled,
586 "os": os,
587 "sys": sys,
588 }
589 installTest = self._dict['Install-test'].strip() + '\n'
590 try:
591 exec installTest in namespace
592 except ImportError, arg:
593 return "no", str(arg)
594 except _scriptExc_NotInstalled, arg:
595 return "no", str(arg)
596 except _scriptExc_OldInstalled, arg:
597 return "old", str(arg)
598 except _scriptExc_BadInstalled, arg:
599 return "bad", str(arg)
600 except:
601 sys.stderr.write("-------------------------------------\n")
602 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
603 sys.stderr.write("---- source:\n")
604 sys.stderr.write(installTest)
605 sys.stderr.write("---- exception:\n")
606 import traceback
607 traceback.print_exc(file=sys.stderr)
608 if self._db._maintainer:
609 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
610 sys.stderr.write("-------------------------------------\n")
611 return "bad", "Package install test got exception"
612 return "yes", ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000613
Jack Jansen0ae32202003-04-09 13:25:43 +0000614 def prerequisites(self):
615 """Return a list of prerequisites for this package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000616
Jack Jansen0ae32202003-04-09 13:25:43 +0000617 The list contains 2-tuples, of which the first item is either
618 a PimpPackage object or None, and the second is a descriptive
619 string. The first item can be None if this package depends on
620 something that isn't pimp-installable, in which case the descriptive
621 string should tell the user what to do."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000622
Jack Jansen0ae32202003-04-09 13:25:43 +0000623 rv = []
624 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000625 # For pseudo-packages that are already installed we don't
626 # return an error message
627 status, _ = self.installed()
628 if status == "yes":
629 return []
Tim Peters182b5ac2004-07-18 06:16:08 +0000630 return [(None,
Jack Jansenaf304a62005-01-03 15:44:18 +0000631 "Package %s cannot be installed automatically, see the description" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000632 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000633 if self.systemwideOnly() and self._db.preferences.isUserInstall():
634 return [(None,
Jack Jansenaf304a62005-01-03 15:44:18 +0000635 "Package %s can only be installed system-wide" %
Jack Jansen5da131b2003-06-01 20:57:12 +0000636 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000637 if not self._dict.get('Prerequisites'):
638 return []
639 for item in self._dict['Prerequisites']:
640 if type(item) == str:
641 pkg = None
642 descr = str(item)
643 else:
644 name = item['Name']
645 if item.has_key('Version'):
646 name = name + '-' + item['Version']
647 if item.has_key('Flavor'):
648 name = name + '-' + item['Flavor']
649 pkg = self._db.find(name)
650 if not pkg:
651 descr = "Requires unknown %s"%name
652 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000653 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000654 rv.append((pkg, descr))
655 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000656
657
Jack Jansen0ae32202003-04-09 13:25:43 +0000658 def downloadPackageOnly(self, output=None):
659 """Download a single package, if needed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000660
Jack Jansen0ae32202003-04-09 13:25:43 +0000661 An MD5 signature is used to determine whether download is needed,
662 and to test that we actually downloaded what we expected.
663 If output is given it is a file-like object that will receive a log
664 of what happens.
Tim Peters182b5ac2004-07-18 06:16:08 +0000665
Jack Jansen0ae32202003-04-09 13:25:43 +0000666 If anything unforeseen happened the method returns an error message
667 string.
668 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000669
Jack Jansen0ae32202003-04-09 13:25:43 +0000670 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
671 path = urllib.url2pathname(path)
672 filename = os.path.split(path)[1]
Tim Peters182b5ac2004-07-18 06:16:08 +0000673 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
Jack Jansen0ae32202003-04-09 13:25:43 +0000674 if not self._archiveOK():
675 if scheme == 'manual':
676 return "Please download package manually and save as %s" % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000677 downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000678 watcher=self._db.preferences.watcher)
679 if not downloader.download(self._dict['Download-URL'],
680 self.archiveFilename, output):
Jack Jansen0ae32202003-04-09 13:25:43 +0000681 return "download command failed"
682 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
683 return "archive not found after download"
684 if not self._archiveOK():
685 return "archive does not have correct MD5 checksum"
Tim Peters182b5ac2004-07-18 06:16:08 +0000686
Jack Jansen0ae32202003-04-09 13:25:43 +0000687 def _archiveOK(self):
688 """Test an archive. It should exist and the MD5 checksum should be correct."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000689
Jack Jansen0ae32202003-04-09 13:25:43 +0000690 if not os.path.exists(self.archiveFilename):
691 return 0
692 if not self._dict.get('MD5Sum'):
693 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
694 return 1
695 data = open(self.archiveFilename, 'rb').read()
696 checksum = md5.new(data).hexdigest()
697 return checksum == self._dict['MD5Sum']
Tim Peters182b5ac2004-07-18 06:16:08 +0000698
Jack Jansen0ae32202003-04-09 13:25:43 +0000699 def unpackPackageOnly(self, output=None):
700 """Unpack a downloaded package archive."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000701
Jack Jansen0ae32202003-04-09 13:25:43 +0000702 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000703 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000704 if filename[-len(ext):] == ext:
705 break
706 else:
707 return "unknown extension for archive file: %s" % filename
708 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000709 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000710 watcher=self._db.preferences.watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000711 rv = unpacker.unpack(self.archiveFilename, output=output)
712 if rv:
713 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000714
Jack Jansen0ae32202003-04-09 13:25:43 +0000715 def installPackageOnly(self, output=None):
716 """Default install method, to be overridden by subclasses"""
717 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
718 % (self.fullname(), self._dict.get(flavor, ""))
Tim Peters182b5ac2004-07-18 06:16:08 +0000719
Jack Jansen0ae32202003-04-09 13:25:43 +0000720 def installSinglePackage(self, output=None):
721 """Download, unpack and install a single package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000722
Jack Jansen0ae32202003-04-09 13:25:43 +0000723 If output is given it should be a file-like object and it
724 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000725
Jack Jansen749f4812003-07-21 20:47:11 +0000726 if not self._dict.get('Download-URL'):
727 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000728 msg = self.downloadPackageOnly(output)
729 if msg:
730 return "%s: download: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000731
Jack Jansen0ae32202003-04-09 13:25:43 +0000732 msg = self.unpackPackageOnly(output)
733 if msg:
734 return "%s: unpack: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000735
Jack Jansen0ae32202003-04-09 13:25:43 +0000736 return self.installPackageOnly(output)
Tim Peters182b5ac2004-07-18 06:16:08 +0000737
Jack Jansen0ae32202003-04-09 13:25:43 +0000738 def beforeInstall(self):
739 """Bookkeeping before installation: remember what we have in site-packages"""
740 self._old_contents = os.listdir(self._db.preferences.installDir)
Tim Peters182b5ac2004-07-18 06:16:08 +0000741
Jack Jansen0ae32202003-04-09 13:25:43 +0000742 def afterInstall(self):
743 """Bookkeeping after installation: interpret any new .pth files that have
744 appeared"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000745
Jack Jansen0ae32202003-04-09 13:25:43 +0000746 new_contents = os.listdir(self._db.preferences.installDir)
747 for fn in new_contents:
748 if fn in self._old_contents:
749 continue
750 if fn[-4:] != '.pth':
751 continue
752 fullname = os.path.join(self._db.preferences.installDir, fn)
753 f = open(fullname)
754 for line in f.readlines():
755 if not line:
756 continue
757 if line[0] == '#':
758 continue
759 if line[:6] == 'import':
760 exec line
761 continue
762 if line[-1] == '\n':
763 line = line[:-1]
764 if not os.path.isabs(line):
765 line = os.path.join(self._db.preferences.installDir, line)
766 line = os.path.realpath(line)
767 if not line in sys.path:
Tim Peters182b5ac2004-07-18 06:16:08 +0000768 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000769
Jack Jansen5da131b2003-06-01 20:57:12 +0000770 def filterExpectedSkips(self, names):
771 """Return a list that contains only unpexpected skips"""
772 if not self._db.preferences.isUserInstall():
773 return names
774 expected_skips = self._dict.get('User-install-skips')
775 if not expected_skips:
776 return names
777 newnames = []
778 for name in names:
779 for skip in expected_skips:
780 if name[:len(skip)] == skip:
781 break
782 else:
783 newnames.append(name)
784 return newnames
785
Jack Jansen0dacac42003-02-14 14:11:59 +0000786class PimpPackage_binary(PimpPackage):
787
Jack Jansen0ae32202003-04-09 13:25:43 +0000788 def unpackPackageOnly(self, output=None):
789 """We don't unpack binary packages until installing"""
790 pass
Tim Peters182b5ac2004-07-18 06:16:08 +0000791
Jack Jansen0ae32202003-04-09 13:25:43 +0000792 def installPackageOnly(self, output=None):
793 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000794
Jack Jansen0ae32202003-04-09 13:25:43 +0000795 If output is given it should be a file-like object and it
796 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000797
Jack Jansen0ae32202003-04-09 13:25:43 +0000798 if self._dict.has_key('Install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000799 return "%s: Binary package cannot have Install-command" % self.fullname()
Tim Peters182b5ac2004-07-18 06:16:08 +0000800
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000801 if self._dict.has_key('Pre-install-command'):
Jack Jansenaf304a62005-01-03 15:44:18 +0000802 if _cmd(output, '/tmp', self._dict['Pre-install-command']):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000803 return "pre-install %s: running \"%s\" failed" % \
804 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000805
Jack Jansen0ae32202003-04-09 13:25:43 +0000806 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000807
Jack Jansen0ae32202003-04-09 13:25:43 +0000808 # Install by unpacking
809 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000810 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000811 if filename[-len(ext):] == ext:
812 break
813 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000814 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
815 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000816
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000817 install_renames = []
818 for k, newloc in self._db.preferences.installLocations:
819 if not newloc:
820 continue
821 if k == "--install-lib":
822 oldloc = DEFAULT_INSTALLDIR
823 else:
824 return "%s: Don't know installLocation %s" % (self.fullname(), k)
825 install_renames.append((oldloc, newloc))
Tim Peters182b5ac2004-07-18 06:16:08 +0000826
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000827 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000828 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000829 if rv:
830 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000831
Jack Jansen0ae32202003-04-09 13:25:43 +0000832 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000833
Jack Jansen0ae32202003-04-09 13:25:43 +0000834 if self._dict.has_key('Post-install-command'):
Jack Jansenaf304a62005-01-03 15:44:18 +0000835 if _cmd(output, '/tmp', self._dict['Post-install-command']):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000836 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000837 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000838
Jack Jansen0ae32202003-04-09 13:25:43 +0000839 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000840
841
Jack Jansen0dacac42003-02-14 14:11:59 +0000842class PimpPackage_source(PimpPackage):
843
Jack Jansen0ae32202003-04-09 13:25:43 +0000844 def unpackPackageOnly(self, output=None):
845 """Unpack a source package and check that setup.py exists"""
846 PimpPackage.unpackPackageOnly(self, output)
847 # Test that a setup script has been create
848 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
849 setupname = os.path.join(self._buildDirname, "setup.py")
850 if not os.path.exists(setupname) and not NO_EXECUTE:
851 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000852
Jack Jansen0ae32202003-04-09 13:25:43 +0000853 def installPackageOnly(self, output=None):
854 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000855
Jack Jansen0ae32202003-04-09 13:25:43 +0000856 If output is given it should be a file-like object and it
857 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000858
Jack Jansen0ae32202003-04-09 13:25:43 +0000859 if self._dict.has_key('Pre-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000860 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000861 return "pre-install %s: running \"%s\" failed" % \
862 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000863
Jack Jansen0ae32202003-04-09 13:25:43 +0000864 self.beforeInstall()
865 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000866 if installcmd and self._install_renames:
867 return "Package has install-command and can only be installed to standard location"
868 # This is the "bit-bucket" for installations: everything we don't
869 # want. After installation we check that it is actually empty
870 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000871 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000872 extra_args = ""
873 for k, v in self._db.preferences.installLocations:
874 if not v:
875 # We don't want these files installed. Send them
876 # to the bit-bucket.
877 if not unwanted_install_dir:
878 unwanted_install_dir = tempfile.mkdtemp()
879 v = unwanted_install_dir
880 extra_args = extra_args + " %s \"%s\"" % (k, v)
881 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
882 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000883 return "install %s: running \"%s\" failed" % \
884 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000885 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
886 unwanted_files = os.listdir(unwanted_install_dir)
887 if unwanted_files:
888 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
889 else:
890 rv = None
891 shutil.rmtree(unwanted_install_dir)
892 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000893
Jack Jansen0ae32202003-04-09 13:25:43 +0000894 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000895
Jack Jansen0ae32202003-04-09 13:25:43 +0000896 if self._dict.has_key('Post-install-command'):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000897 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000898 return "post-install %s: running \"%s\" failed" % \
899 (self.fullname(), self._dict['Post-install-command'])
900 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000901
Jack Jansenaf304a62005-01-03 15:44:18 +0000902class PimpPackage_installer(PimpPackage):
903
904 def unpackPackageOnly(self, output=None):
905 """We don't unpack dmg packages until installing"""
906 pass
907
908 def installPackageOnly(self, output=None):
909 """Install a single source package.
910
911 If output is given it should be a file-like object and it
912 will receive a log of what happened."""
913
914 if self._dict.has_key('Post-install-command'):
915 return "%s: Installer package cannot have Post-install-command" % self.fullname()
916
917 if self._dict.has_key('Pre-install-command'):
918 if _cmd(output, '/tmp', self._dict['Pre-install-command']):
919 return "pre-install %s: running \"%s\" failed" % \
920 (self.fullname(), self._dict['Pre-install-command'])
921
922 self.beforeInstall()
923
924 installcmd = self._dict.get('Install-command')
925 if installcmd:
926 if '%' in installcmd:
927 installcmd = installcmd % self.archiveFilename
928 else:
929 installcmd = 'open \"%s\"' % self.archiveFilename
930 if _cmd(output, "/tmp", installcmd):
931 return '%s: install command failed (use verbose for details)' % self.fullname()
932 return '%s: downloaded and opened. Install manually and restart Package Manager' % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000933
Jack Jansen95839b82003-02-09 23:10:20 +0000934class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000935 """Installer engine: computes dependencies and installs
936 packages in the right order."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000937
Jack Jansen0ae32202003-04-09 13:25:43 +0000938 def __init__(self, db):
939 self._todo = []
940 self._db = db
941 self._curtodo = []
942 self._curmessages = []
Tim Peters182b5ac2004-07-18 06:16:08 +0000943
Jack Jansen0ae32202003-04-09 13:25:43 +0000944 def __contains__(self, package):
945 return package in self._todo
Tim Peters182b5ac2004-07-18 06:16:08 +0000946
Jack Jansen0ae32202003-04-09 13:25:43 +0000947 def _addPackages(self, packages):
948 for package in packages:
949 if not package in self._todo:
Jack Jansen118e1272004-03-13 23:50:48 +0000950 self._todo.append(package)
Tim Peters182b5ac2004-07-18 06:16:08 +0000951
Jack Jansen0ae32202003-04-09 13:25:43 +0000952 def _prepareInstall(self, package, force=0, recursive=1):
953 """Internal routine, recursive engine for prepareInstall.
Tim Peters182b5ac2004-07-18 06:16:08 +0000954
Jack Jansen0ae32202003-04-09 13:25:43 +0000955 Test whether the package is installed and (if not installed
956 or if force==1) prepend it to the temporary todo list and
957 call ourselves recursively on all prerequisites."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000958
Jack Jansen0ae32202003-04-09 13:25:43 +0000959 if not force:
960 status, message = package.installed()
961 if status == "yes":
Tim Peters182b5ac2004-07-18 06:16:08 +0000962 return
Jack Jansen0ae32202003-04-09 13:25:43 +0000963 if package in self._todo or package in self._curtodo:
964 return
965 self._curtodo.insert(0, package)
966 if not recursive:
967 return
968 prereqs = package.prerequisites()
969 for pkg, descr in prereqs:
970 if pkg:
Jack Jansen0576d0a2004-03-13 23:03:38 +0000971 self._prepareInstall(pkg, False, recursive)
Jack Jansen0ae32202003-04-09 13:25:43 +0000972 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000973 self._curmessages.append("Problem with dependency: %s" % descr)
Tim Peters182b5ac2004-07-18 06:16:08 +0000974
Jack Jansen0ae32202003-04-09 13:25:43 +0000975 def prepareInstall(self, package, force=0, recursive=1):
976 """Prepare installation of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000977
Jack Jansen0ae32202003-04-09 13:25:43 +0000978 If the package is already installed and force is false nothing
979 is done. If recursive is true prerequisites are installed first.
Tim Peters182b5ac2004-07-18 06:16:08 +0000980
Jack Jansen0ae32202003-04-09 13:25:43 +0000981 Returns a list of packages (to be passed to install) and a list
982 of messages of any problems encountered.
983 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000984
Jack Jansen0ae32202003-04-09 13:25:43 +0000985 self._curtodo = []
986 self._curmessages = []
987 self._prepareInstall(package, force, recursive)
988 rv = self._curtodo, self._curmessages
989 self._curtodo = []
990 self._curmessages = []
991 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000992
Jack Jansen0ae32202003-04-09 13:25:43 +0000993 def install(self, packages, output):
994 """Install a list of packages."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000995
Jack Jansen0ae32202003-04-09 13:25:43 +0000996 self._addPackages(packages)
997 status = []
998 for pkg in self._todo:
999 msg = pkg.installSinglePackage(output)
1000 if msg:
1001 status.append(msg)
1002 return status
Tim Peters182b5ac2004-07-18 06:16:08 +00001003
1004
1005
Jack Jansen989ddc02004-03-11 23:03:59 +00001006def _run(mode, verbose, force, args, prefargs, watcher):
Jack Jansen0ae32202003-04-09 13:25:43 +00001007 """Engine for the main program"""
Tim Peters182b5ac2004-07-18 06:16:08 +00001008
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001009 prefs = PimpPreferences(**prefargs)
Jack Jansen989ddc02004-03-11 23:03:59 +00001010 if watcher:
1011 prefs.setWatcher(watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001012 rv = prefs.check()
1013 if rv:
1014 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +00001015 db = PimpDatabase(prefs)
1016 db.appendURL(prefs.pimpDatabase)
Tim Peters182b5ac2004-07-18 06:16:08 +00001017
Jack Jansen0ae32202003-04-09 13:25:43 +00001018 if mode == 'dump':
1019 db.dump(sys.stdout)
1020 elif mode =='list':
1021 if not args:
1022 args = db.listnames()
1023 print "%-20.20s\t%s" % ("Package", "Description")
1024 print
1025 for pkgname in args:
1026 pkg = db.find(pkgname)
1027 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001028 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +00001029 pkgname = pkg.fullname()
1030 else:
1031 description = 'Error: no such package'
1032 print "%-20.20s\t%s" % (pkgname, description)
1033 if verbose:
1034 print "\tHome page:\t", pkg.homepage()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001035 try:
1036 print "\tDownload URL:\t", pkg.downloadURL()
1037 except KeyError:
1038 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +00001039 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001040 description = '\n\t\t\t\t\t'.join(description.splitlines())
Jack Jansen9f0c5752003-05-29 22:07:27 +00001041 print "\tDescription:\t%s" % description
Jack Jansen0ae32202003-04-09 13:25:43 +00001042 elif mode =='status':
1043 if not args:
1044 args = db.listnames()
1045 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
1046 print
1047 for pkgname in args:
1048 pkg = db.find(pkgname)
1049 if pkg:
1050 status, msg = pkg.installed()
1051 pkgname = pkg.fullname()
1052 else:
1053 status = 'error'
1054 msg = 'No such package'
1055 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
1056 if verbose and status == "no":
1057 prereq = pkg.prerequisites()
1058 for pkg, msg in prereq:
1059 if not pkg:
1060 pkg = ''
1061 else:
1062 pkg = pkg.fullname()
1063 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
1064 elif mode == 'install':
1065 if not args:
1066 print 'Please specify packages to install'
1067 sys.exit(1)
1068 inst = PimpInstaller(db)
1069 for pkgname in args:
1070 pkg = db.find(pkgname)
1071 if not pkg:
1072 print '%s: No such package' % pkgname
1073 continue
1074 list, messages = inst.prepareInstall(pkg, force)
1075 if messages and not force:
1076 print "%s: Not installed:" % pkgname
1077 for m in messages:
1078 print "\t", m
1079 else:
1080 if verbose:
1081 output = sys.stdout
1082 else:
1083 output = None
1084 messages = inst.install(list, output)
1085 if messages:
1086 print "%s: Not installed:" % pkgname
1087 for m in messages:
1088 print "\t", m
Jack Jansen95839b82003-02-09 23:10:20 +00001089
1090def main():
Jack Jansen0ae32202003-04-09 13:25:43 +00001091 """Minimal commandline tool to drive pimp."""
Tim Peters182b5ac2004-07-18 06:16:08 +00001092
Jack Jansen0ae32202003-04-09 13:25:43 +00001093 import getopt
1094 def _help():
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001095 print "Usage: pimp [options] -s [package ...] List installed status"
1096 print " pimp [options] -l [package ...] Show package information"
1097 print " pimp [options] -i package ... Install packages"
1098 print " pimp -d Dump database to stdout"
Jack Jansenb789a062003-05-28 18:56:30 +00001099 print " pimp -V Print version number"
Jack Jansen0ae32202003-04-09 13:25:43 +00001100 print "Options:"
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001101 print " -v Verbose"
1102 print " -f Force installation"
Jack Jansenb789a062003-05-28 18:56:30 +00001103 print " -D dir Set destination directory"
1104 print " (default: %s)" % DEFAULT_INSTALLDIR
1105 print " -u url URL for database"
Jack Jansen0ae32202003-04-09 13:25:43 +00001106 sys.exit(1)
Tim Peters182b5ac2004-07-18 06:16:08 +00001107
Jack Jansen989ddc02004-03-11 23:03:59 +00001108 class _Watcher:
1109 def update(self, msg):
1110 sys.stderr.write(msg + '\r')
1111 return 1
Tim Peters182b5ac2004-07-18 06:16:08 +00001112
Jack Jansen0ae32202003-04-09 13:25:43 +00001113 try:
Jack Jansenb789a062003-05-28 18:56:30 +00001114 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
1115 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +00001116 _help()
1117 if not opts and not args:
1118 _help()
1119 mode = None
1120 force = 0
1121 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001122 prefargs = {}
Jack Jansen989ddc02004-03-11 23:03:59 +00001123 watcher = None
Jack Jansen0ae32202003-04-09 13:25:43 +00001124 for o, a in opts:
1125 if o == '-s':
1126 if mode:
1127 _help()
1128 mode = 'status'
1129 if o == '-l':
1130 if mode:
1131 _help()
1132 mode = 'list'
1133 if o == '-d':
1134 if mode:
1135 _help()
1136 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001137 if o == '-V':
1138 if mode:
1139 _help()
1140 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001141 if o == '-i':
1142 mode = 'install'
1143 if o == '-f':
1144 force = 1
1145 if o == '-v':
1146 verbose = 1
Jack Jansen989ddc02004-03-11 23:03:59 +00001147 watcher = _Watcher()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001148 if o == '-D':
1149 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001150 if o == '-u':
1151 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001152 if not mode:
1153 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001154 if mode == 'version':
1155 print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1156 else:
Jack Jansen989ddc02004-03-11 23:03:59 +00001157 _run(mode, verbose, force, args, prefargs, watcher)
Jack Jansenb789a062003-05-28 18:56:30 +00001158
1159# Finally, try to update ourselves to a newer version.
1160# If the end-user updates pimp through pimp the new version
1161# will be called pimp_update and live in site-packages
1162# or somewhere similar
1163if __name__ != 'pimp_update':
1164 try:
1165 import pimp_update
1166 except ImportError:
1167 pass
1168 else:
1169 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1170 import warnings
1171 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1172 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1173 else:
1174 from pimp_update import *
Tim Peters182b5ac2004-07-18 06:16:08 +00001175
Jack Jansen95839b82003-02-09 23:10:20 +00001176if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001177 main()