blob: e52d0dce349583fdd2dfd33917bfd60b6cbb10f7 [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
Guido van Rossum360e4b82007-05-14 22:51:27 +000017import subprocess
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
Thomas Wouters477c8d52006-05-27 19:21:47 +000024import hashlib
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)
Guido van Rossumb940e112007-01-10 16:19:56 +000079 except urllib2.HTTPError as arg:
Jack Jansen989ddc02004-03-11 23:03:59 +000080 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
Guido van Rossum360e4b82007-05-14 22:51:27 +0000104 child = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
105 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
106 child.stdin.close()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000107 while 1:
Guido van Rossum360e4b82007-05-14 22:51:27 +0000108 line = child.stdout.readline()
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000109 if not line:
110 break
111 if output:
112 output.write(line)
113 return child.wait()
114
Jack Jansen989ddc02004-03-11 23:03:59 +0000115class PimpDownloader:
116 """Abstract base class - Downloader for archives"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000117
Jack Jansen989ddc02004-03-11 23:03:59 +0000118 def __init__(self, argument,
119 dir="",
120 watcher=None):
121 self.argument = argument
122 self._dir = dir
123 self._watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000124
Jack Jansen989ddc02004-03-11 23:03:59 +0000125 def download(self, url, filename, output=None):
126 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000127
Jack Jansen989ddc02004-03-11 23:03:59 +0000128 def update(self, str):
129 if self._watcher:
130 return self._watcher.update(str)
131 return True
Tim Peters182b5ac2004-07-18 06:16:08 +0000132
Jack Jansen989ddc02004-03-11 23:03:59 +0000133class PimpCurlDownloader(PimpDownloader):
134
135 def download(self, url, filename, output=None):
136 self.update("Downloading %s..." % url)
137 exitstatus = _cmd(output, self._dir,
138 "curl",
139 "--output", filename,
140 url)
141 self.update("Downloading %s: finished" % url)
142 return (not exitstatus)
Tim Peters182b5ac2004-07-18 06:16:08 +0000143
Jack Jansen989ddc02004-03-11 23:03:59 +0000144class PimpUrllibDownloader(PimpDownloader):
145
146 def download(self, url, filename, output=None):
147 output = open(filename, 'wb')
148 self.update("Downloading %s: opening connection" % url)
149 keepgoing = True
150 download = urllib2.urlopen(url)
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000151 if "content-length" in download.headers:
Guido van Rossume2a383d2007-01-15 16:59:06 +0000152 length = int(download.headers['content-length'])
Jack Jansen989ddc02004-03-11 23:03:59 +0000153 else:
154 length = -1
Tim Peters182b5ac2004-07-18 06:16:08 +0000155
Jack Jansen989ddc02004-03-11 23:03:59 +0000156 data = download.read(4096) #read 4K at a time
157 dlsize = 0
158 lasttime = 0
159 while keepgoing:
160 dlsize = dlsize + len(data)
Tim Peters182b5ac2004-07-18 06:16:08 +0000161 if len(data) == 0:
Jack Jansen989ddc02004-03-11 23:03:59 +0000162 #this is our exit condition
163 break
164 output.write(data)
165 if int(time.time()) != lasttime:
166 # Update at most once per second
167 lasttime = int(time.time())
168 if length == -1:
169 keepgoing = self.update("Downloading %s: %d bytes..." % (url, dlsize))
170 else:
171 keepgoing = self.update("Downloading %s: %d%% (%d bytes)..." % (url, int(100.0*dlsize/length), dlsize))
172 data = download.read(4096)
173 if keepgoing:
174 self.update("Downloading %s: finished" % url)
175 return keepgoing
Tim Peters182b5ac2004-07-18 06:16:08 +0000176
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000177class PimpUnpacker:
178 """Abstract base class - Unpacker for archives"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000179
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000180 _can_rename = False
Tim Peters182b5ac2004-07-18 06:16:08 +0000181
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000182 def __init__(self, argument,
183 dir="",
Jack Jansen989ddc02004-03-11 23:03:59 +0000184 renames=[],
185 watcher=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000186 self.argument = argument
187 if renames and not self._can_rename:
Collin Wintere45be282007-08-23 00:01:55 +0000188 raise RuntimeError("This unpacker cannot rename files")
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000189 self._dir = dir
190 self._renames = renames
Jack Jansen989ddc02004-03-11 23:03:59 +0000191 self._watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000192
Jack Jansen5da131b2003-06-01 20:57:12 +0000193 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000194 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000195
Jack Jansen989ddc02004-03-11 23:03:59 +0000196 def update(self, str):
197 if self._watcher:
198 return self._watcher.update(str)
199 return True
Tim Peters182b5ac2004-07-18 06:16:08 +0000200
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000201class PimpCommandUnpacker(PimpUnpacker):
202 """Unpack archives by calling a Unix utility"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000203
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000204 _can_rename = False
Tim Peters182b5ac2004-07-18 06:16:08 +0000205
Jack Jansen5da131b2003-06-01 20:57:12 +0000206 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000207 cmd = self.argument % archive
208 if _cmd(output, self._dir, cmd):
209 return "unpack command failed"
Tim Peters182b5ac2004-07-18 06:16:08 +0000210
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000211class PimpTarUnpacker(PimpUnpacker):
212 """Unpack tarfiles using the builtin tarfile module"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000213
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000214 _can_rename = True
Tim Peters182b5ac2004-07-18 06:16:08 +0000215
Jack Jansen5da131b2003-06-01 20:57:12 +0000216 def unpack(self, archive, output=None, package=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000217 tf = tarfile.open(archive, "r")
218 members = tf.getmembers()
219 skip = []
220 if self._renames:
221 for member in members:
222 for oldprefix, newprefix in self._renames:
223 if oldprefix[:len(self._dir)] == self._dir:
224 oldprefix2 = oldprefix[len(self._dir):]
225 else:
226 oldprefix2 = None
227 if member.name[:len(oldprefix)] == oldprefix:
228 if newprefix is None:
229 skip.append(member)
230 #print 'SKIP', member.name
231 else:
232 member.name = newprefix + member.name[len(oldprefix):]
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000233 print(' ', member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000234 break
235 elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
236 if newprefix is None:
237 skip.append(member)
238 #print 'SKIP', member.name
239 else:
240 member.name = newprefix + member.name[len(oldprefix2):]
241 #print ' ', member.name
242 break
243 else:
244 skip.append(member)
245 #print '????', member.name
246 for member in members:
247 if member in skip:
Jack Jansen989ddc02004-03-11 23:03:59 +0000248 self.update("Skipping %s" % member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000249 continue
Jack Jansen989ddc02004-03-11 23:03:59 +0000250 self.update("Extracting %s" % member.name)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000251 tf.extract(member, self._dir)
252 if skip:
253 names = [member.name for member in skip if member.name[-1] != '/']
Jack Jansen5da131b2003-06-01 20:57:12 +0000254 if package:
255 names = package.filterExpectedSkips(names)
Jack Jansen6432f782003-04-22 13:56:19 +0000256 if names:
Jack Jansen705553a2003-05-06 12:44:00 +0000257 return "Not all files were unpacked: %s" % " ".join(names)
Tim Peters182b5ac2004-07-18 06:16:08 +0000258
Jack Jansen95839b82003-02-09 23:10:20 +0000259ARCHIVE_FORMATS = [
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000260 (".tar.Z", PimpTarUnpacker, None),
261 (".taz", PimpTarUnpacker, None),
262 (".tar.gz", PimpTarUnpacker, None),
263 (".tgz", PimpTarUnpacker, None),
264 (".tar.bz", PimpTarUnpacker, None),
265 (".zip", PimpCommandUnpacker, "unzip \"%s\""),
Jack Jansen95839b82003-02-09 23:10:20 +0000266]
267
268class PimpPreferences:
Jack Jansen0ae32202003-04-09 13:25:43 +0000269 """Container for per-user preferences, such as the database to use
270 and where to install packages."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000271
272 def __init__(self,
Jack Jansen0ae32202003-04-09 13:25:43 +0000273 flavorOrder=None,
274 downloadDir=None,
275 buildDir=None,
276 installDir=None,
277 pimpDatabase=None):
278 if not flavorOrder:
279 flavorOrder = DEFAULT_FLAVORORDER
280 if not downloadDir:
281 downloadDir = DEFAULT_DOWNLOADDIR
282 if not buildDir:
283 buildDir = DEFAULT_BUILDDIR
Jack Jansen0ae32202003-04-09 13:25:43 +0000284 if not pimpDatabase:
Jack Jansen192bd962004-02-28 23:18:43 +0000285 pimpDatabase = getDefaultDatabase()
Jack Jansen20fa6752003-04-16 12:15:34 +0000286 self.setInstallDir(installDir)
287 self.flavorOrder = flavorOrder
288 self.downloadDir = downloadDir
289 self.buildDir = buildDir
290 self.pimpDatabase = pimpDatabase
Jack Jansen989ddc02004-03-11 23:03:59 +0000291 self.watcher = None
Tim Peters182b5ac2004-07-18 06:16:08 +0000292
Jack Jansen989ddc02004-03-11 23:03:59 +0000293 def setWatcher(self, watcher):
294 self.watcher = watcher
Tim Peters182b5ac2004-07-18 06:16:08 +0000295
Jack Jansen20fa6752003-04-16 12:15:34 +0000296 def setInstallDir(self, installDir=None):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000297 if installDir:
298 # Installing to non-standard location.
299 self.installLocations = [
300 ('--install-lib', installDir),
301 ('--install-headers', None),
302 ('--install-scripts', None),
303 ('--install-data', None)]
304 else:
305 installDir = DEFAULT_INSTALLDIR
306 self.installLocations = []
Jack Jansen0ae32202003-04-09 13:25:43 +0000307 self.installDir = installDir
Tim Peters182b5ac2004-07-18 06:16:08 +0000308
Jack Jansen5da131b2003-06-01 20:57:12 +0000309 def isUserInstall(self):
310 return self.installDir != DEFAULT_INSTALLDIR
Jack Jansen20fa6752003-04-16 12:15:34 +0000311
Jack Jansen0ae32202003-04-09 13:25:43 +0000312 def check(self):
313 """Check that the preferences make sense: directories exist and are
314 writable, the install directory is on sys.path, etc."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000315
Jack Jansen0ae32202003-04-09 13:25:43 +0000316 rv = ""
317 RWX_OK = os.R_OK|os.W_OK|os.X_OK
318 if not os.path.exists(self.downloadDir):
319 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
320 elif not os.access(self.downloadDir, RWX_OK):
321 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
322 if not os.path.exists(self.buildDir):
323 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
324 elif not os.access(self.buildDir, RWX_OK):
325 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
326 if not os.path.exists(self.installDir):
327 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
328 elif not os.access(self.installDir, RWX_OK):
329 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
330 else:
331 installDir = os.path.realpath(self.installDir)
332 for p in sys.path:
333 try:
334 realpath = os.path.realpath(p)
335 except:
336 pass
337 if installDir == realpath:
338 break
339 else:
340 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000341 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000342
Jack Jansen0ae32202003-04-09 13:25:43 +0000343 def compareFlavors(self, left, right):
344 """Compare two flavor strings. This is part of your preferences
345 because whether the user prefers installing from source or binary is."""
346 if left in self.flavorOrder:
347 if right in self.flavorOrder:
348 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
349 return -1
350 if right in self.flavorOrder:
351 return 1
352 return cmp(left, right)
Tim Peters182b5ac2004-07-18 06:16:08 +0000353
Jack Jansen95839b82003-02-09 23:10:20 +0000354class PimpDatabase:
Jack Jansen0ae32202003-04-09 13:25:43 +0000355 """Class representing a pimp database. It can actually contain
356 information from multiple databases through inclusion, but the
357 toplevel database is considered the master, as its maintainer is
358 "responsible" for the contents."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000359
Jack Jansen0ae32202003-04-09 13:25:43 +0000360 def __init__(self, prefs):
361 self._packages = []
362 self.preferences = prefs
Jack Jansen118e1272004-03-13 23:50:48 +0000363 self._url = ""
Jack Jansen0ae32202003-04-09 13:25:43 +0000364 self._urllist = []
365 self._version = ""
366 self._maintainer = ""
367 self._description = ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000368
Jack Jansen118e1272004-03-13 23:50:48 +0000369 # Accessor functions
370 def url(self): return self._url
371 def version(self): return self._version
372 def maintainer(self): return self._maintainer
373 def description(self): return self._description
Tim Peters182b5ac2004-07-18 06:16:08 +0000374
Jack Jansen0ae32202003-04-09 13:25:43 +0000375 def close(self):
376 """Clean up"""
377 self._packages = []
378 self.preferences = None
Tim Peters182b5ac2004-07-18 06:16:08 +0000379
Jack Jansen0ae32202003-04-09 13:25:43 +0000380 def appendURL(self, url, included=0):
381 """Append packages from the database with the given URL.
382 Only the first database should specify included=0, so the
383 global information (maintainer, description) get stored."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000384
Jack Jansen0ae32202003-04-09 13:25:43 +0000385 if url in self._urllist:
386 return
387 self._urllist.append(url)
388 fp = urllib2.urlopen(url).fp
Jack Jansenc32cec12004-03-13 23:32:47 +0000389 plistdata = plistlib.Plist.fromFile(fp)
Jack Jansen0ae32202003-04-09 13:25:43 +0000390 # Test here for Pimp version, etc
Jack Jansenb789a062003-05-28 18:56:30 +0000391 if included:
Jack Jansenc32cec12004-03-13 23:32:47 +0000392 version = plistdata.get('Version')
Jack Jansenb789a062003-05-28 18:56:30 +0000393 if version and version > self._version:
394 sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
395 (url, version))
396 else:
Jack Jansenc32cec12004-03-13 23:32:47 +0000397 self._version = plistdata.get('Version')
Jack Jansenb789a062003-05-28 18:56:30 +0000398 if not self._version:
399 sys.stderr.write("Warning: database has no Version information\n")
400 elif self._version > PIMP_VERSION:
Tim Peters182b5ac2004-07-18 06:16:08 +0000401 sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
Jack Jansen0ae32202003-04-09 13:25:43 +0000402 % (self._version, PIMP_VERSION))
Jack Jansenc32cec12004-03-13 23:32:47 +0000403 self._maintainer = plistdata.get('Maintainer', '')
404 self._description = plistdata.get('Description', '').strip()
Jack Jansen118e1272004-03-13 23:50:48 +0000405 self._url = url
Jack Jansen1dd087c2005-01-07 13:43:31 +0000406 self._appendPackages(plistdata['Packages'], url)
Jack Jansenc32cec12004-03-13 23:32:47 +0000407 others = plistdata.get('Include', [])
Jack Jansen1dd087c2005-01-07 13:43:31 +0000408 for o in others:
409 o = urllib.basejoin(url, o)
410 self.appendURL(o, included=1)
Tim Peters182b5ac2004-07-18 06:16:08 +0000411
Jack Jansen1dd087c2005-01-07 13:43:31 +0000412 def _appendPackages(self, packages, url):
Jack Jansen0ae32202003-04-09 13:25:43 +0000413 """Given a list of dictionaries containing package
414 descriptions create the PimpPackage objects and append them
415 to our internal storage."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000416
Jack Jansen0ae32202003-04-09 13:25:43 +0000417 for p in packages:
418 p = dict(p)
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000419 if 'Download-URL' in p:
Jack Jansen1dd087c2005-01-07 13:43:31 +0000420 p['Download-URL'] = urllib.basejoin(url, p['Download-URL'])
Jack Jansen0ae32202003-04-09 13:25:43 +0000421 flavor = p.get('Flavor')
422 if flavor == 'source':
423 pkg = PimpPackage_source(self, p)
424 elif flavor == 'binary':
425 pkg = PimpPackage_binary(self, p)
Jack Jansenaf304a62005-01-03 15:44:18 +0000426 elif flavor == 'installer':
427 pkg = PimpPackage_installer(self, p)
428 elif flavor == 'hidden':
429 pkg = PimpPackage_installer(self, p)
Jack Jansen0ae32202003-04-09 13:25:43 +0000430 else:
431 pkg = PimpPackage(self, dict(p))
432 self._packages.append(pkg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000433
Jack Jansen0ae32202003-04-09 13:25:43 +0000434 def list(self):
435 """Return a list of all PimpPackage objects in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000436
Jack Jansen0ae32202003-04-09 13:25:43 +0000437 return self._packages
Tim Peters182b5ac2004-07-18 06:16:08 +0000438
Jack Jansen0ae32202003-04-09 13:25:43 +0000439 def listnames(self):
440 """Return a list of names of all packages in the database."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000441
Jack Jansen0ae32202003-04-09 13:25:43 +0000442 rv = []
443 for pkg in self._packages:
444 rv.append(pkg.fullname())
445 rv.sort()
446 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000447
Jack Jansen0ae32202003-04-09 13:25:43 +0000448 def dump(self, pathOrFile):
449 """Dump the contents of the database to an XML .plist file.
Tim Peters182b5ac2004-07-18 06:16:08 +0000450
Jack Jansen0ae32202003-04-09 13:25:43 +0000451 The file can be passed as either a file object or a pathname.
452 All data, including included databases, is dumped."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000453
Jack Jansen0ae32202003-04-09 13:25:43 +0000454 packages = []
455 for pkg in self._packages:
456 packages.append(pkg.dump())
Jack Jansenc32cec12004-03-13 23:32:47 +0000457 plistdata = {
Jack Jansen0ae32202003-04-09 13:25:43 +0000458 'Version': self._version,
459 'Maintainer': self._maintainer,
460 'Description': self._description,
461 'Packages': packages
462 }
Jack Jansenc32cec12004-03-13 23:32:47 +0000463 plist = plistlib.Plist(**plistdata)
Jack Jansen0ae32202003-04-09 13:25:43 +0000464 plist.write(pathOrFile)
Tim Peters182b5ac2004-07-18 06:16:08 +0000465
Jack Jansen0ae32202003-04-09 13:25:43 +0000466 def find(self, ident):
467 """Find a package. The package can be specified by name
468 or as a dictionary with name, version and flavor entries.
Tim Peters182b5ac2004-07-18 06:16:08 +0000469
Jack Jansen0ae32202003-04-09 13:25:43 +0000470 Only name is obligatory. If there are multiple matches the
471 best one (higher version number, flavors ordered according to
472 users' preference) is returned."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000473
Jack Jansen0ae32202003-04-09 13:25:43 +0000474 if type(ident) == str:
475 # Remove ( and ) for pseudo-packages
476 if ident[0] == '(' and ident[-1] == ')':
477 ident = ident[1:-1]
478 # Split into name-version-flavor
479 fields = ident.split('-')
480 if len(fields) < 1 or len(fields) > 3:
481 return None
482 name = fields[0]
483 if len(fields) > 1:
484 version = fields[1]
485 else:
486 version = None
487 if len(fields) > 2:
488 flavor = fields[2]
489 else:
490 flavor = None
491 else:
492 name = ident['Name']
493 version = ident.get('Version')
494 flavor = ident.get('Flavor')
495 found = None
496 for p in self._packages:
497 if name == p.name() and \
498 (not version or version == p.version()) and \
499 (not flavor or flavor == p.flavor()):
500 if not found or found < p:
501 found = p
502 return found
Tim Peters182b5ac2004-07-18 06:16:08 +0000503
Jack Jansene7b33db2003-02-11 22:40:59 +0000504ALLOWED_KEYS = [
Jack Jansen0ae32202003-04-09 13:25:43 +0000505 "Name",
506 "Version",
507 "Flavor",
508 "Description",
509 "Home-page",
510 "Download-URL",
511 "Install-test",
512 "Install-command",
513 "Pre-install-command",
514 "Post-install-command",
515 "Prerequisites",
Jack Jansen5da131b2003-06-01 20:57:12 +0000516 "MD5Sum",
517 "User-install-skips",
518 "Systemwide-only",
Jack Jansene7b33db2003-02-11 22:40:59 +0000519]
520
Jack Jansen95839b82003-02-09 23:10:20 +0000521class PimpPackage:
Jack Jansen0ae32202003-04-09 13:25:43 +0000522 """Class representing a single package."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000523
Jack Jansenc32cec12004-03-13 23:32:47 +0000524 def __init__(self, db, plistdata):
Jack Jansen0ae32202003-04-09 13:25:43 +0000525 self._db = db
Jack Jansenc32cec12004-03-13 23:32:47 +0000526 name = plistdata["Name"]
527 for k in plistdata.keys():
Jack Jansen0ae32202003-04-09 13:25:43 +0000528 if not k in ALLOWED_KEYS:
529 sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
Jack Jansenc32cec12004-03-13 23:32:47 +0000530 self._dict = plistdata
Tim Peters182b5ac2004-07-18 06:16:08 +0000531
Jack Jansen0ae32202003-04-09 13:25:43 +0000532 def __getitem__(self, key):
533 return self._dict[key]
Tim Peters182b5ac2004-07-18 06:16:08 +0000534
Jack Jansen0ae32202003-04-09 13:25:43 +0000535 def name(self): return self._dict['Name']
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000536 def version(self): return self._dict.get('Version')
537 def flavor(self): return self._dict.get('Flavor')
Jack Jansen9f0c5752003-05-29 22:07:27 +0000538 def description(self): return self._dict['Description'].strip()
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000539 def shortdescription(self): return self.description().splitlines()[0]
Jack Jansen0ae32202003-04-09 13:25:43 +0000540 def homepage(self): return self._dict.get('Home-page')
Jack Jansenc7c78ae2003-05-06 13:07:32 +0000541 def downloadURL(self): return self._dict.get('Download-URL')
Jack Jansen5da131b2003-06-01 20:57:12 +0000542 def systemwideOnly(self): return self._dict.get('Systemwide-only')
Tim Peters182b5ac2004-07-18 06:16:08 +0000543
Jack Jansen0ae32202003-04-09 13:25:43 +0000544 def fullname(self):
545 """Return the full name "name-version-flavor" of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000546
Jack Jansen0ae32202003-04-09 13:25:43 +0000547 If the package is a pseudo-package, something that cannot be
548 installed through pimp, return the name in (parentheses)."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000549
Jack Jansen0ae32202003-04-09 13:25:43 +0000550 rv = self._dict['Name']
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000551 if 'Version' in self._dict:
Jack Jansen0ae32202003-04-09 13:25:43 +0000552 rv = rv + '-%s' % self._dict['Version']
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000553 if 'Flavor' in self._dict:
Jack Jansen0ae32202003-04-09 13:25:43 +0000554 rv = rv + '-%s' % self._dict['Flavor']
Jack Jansenaf304a62005-01-03 15:44:18 +0000555 if self._dict.get('Flavor') == 'hidden':
Jack Jansen0ae32202003-04-09 13:25:43 +0000556 # Pseudo-package, show in parentheses
557 rv = '(%s)' % rv
558 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000559
Jack Jansen0ae32202003-04-09 13:25:43 +0000560 def dump(self):
561 """Return a dict object containing the information on the package."""
562 return self._dict
Tim Peters182b5ac2004-07-18 06:16:08 +0000563
Jack Jansen0ae32202003-04-09 13:25:43 +0000564 def __cmp__(self, other):
565 """Compare two packages, where the "better" package sorts lower."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000566
Jack Jansen0ae32202003-04-09 13:25:43 +0000567 if not isinstance(other, PimpPackage):
568 return cmp(id(self), id(other))
569 if self.name() != other.name():
570 return cmp(self.name(), other.name())
571 if self.version() != other.version():
572 return -cmp(self.version(), other.version())
573 return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
Tim Peters182b5ac2004-07-18 06:16:08 +0000574
Jack Jansen0ae32202003-04-09 13:25:43 +0000575 def installed(self):
576 """Test wheter the package is installed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000577
Jack Jansen0ae32202003-04-09 13:25:43 +0000578 Returns two values: a status indicator which is one of
579 "yes", "no", "old" (an older version is installed) or "bad"
580 (something went wrong during the install test) and a human
581 readable string which may contain more details."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000582
Jack Jansen0ae32202003-04-09 13:25:43 +0000583 namespace = {
584 "NotInstalled": _scriptExc_NotInstalled,
585 "OldInstalled": _scriptExc_OldInstalled,
586 "BadInstalled": _scriptExc_BadInstalled,
587 "os": os,
588 "sys": sys,
589 }
590 installTest = self._dict['Install-test'].strip() + '\n'
591 try:
Georg Brandl7cae87c2006-09-06 06:51:57 +0000592 exec(installTest, namespace)
Guido van Rossumb940e112007-01-10 16:19:56 +0000593 except ImportError as arg:
Jack Jansen0ae32202003-04-09 13:25:43 +0000594 return "no", str(arg)
Guido van Rossumb940e112007-01-10 16:19:56 +0000595 except _scriptExc_NotInstalled as arg:
Jack Jansen0ae32202003-04-09 13:25:43 +0000596 return "no", str(arg)
Guido van Rossumb940e112007-01-10 16:19:56 +0000597 except _scriptExc_OldInstalled as arg:
Jack Jansen0ae32202003-04-09 13:25:43 +0000598 return "old", str(arg)
Guido van Rossumb940e112007-01-10 16:19:56 +0000599 except _scriptExc_BadInstalled as arg:
Jack Jansen0ae32202003-04-09 13:25:43 +0000600 return "bad", str(arg)
601 except:
602 sys.stderr.write("-------------------------------------\n")
603 sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
604 sys.stderr.write("---- source:\n")
605 sys.stderr.write(installTest)
606 sys.stderr.write("---- exception:\n")
607 import traceback
608 traceback.print_exc(file=sys.stderr)
609 if self._db._maintainer:
610 sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
611 sys.stderr.write("-------------------------------------\n")
612 return "bad", "Package install test got exception"
613 return "yes", ""
Tim Peters182b5ac2004-07-18 06:16:08 +0000614
Jack Jansen0ae32202003-04-09 13:25:43 +0000615 def prerequisites(self):
616 """Return a list of prerequisites for this package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000617
Jack Jansen0ae32202003-04-09 13:25:43 +0000618 The list contains 2-tuples, of which the first item is either
619 a PimpPackage object or None, and the second is a descriptive
620 string. The first item can be None if this package depends on
621 something that isn't pimp-installable, in which case the descriptive
622 string should tell the user what to do."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000623
Jack Jansen0ae32202003-04-09 13:25:43 +0000624 rv = []
625 if not self._dict.get('Download-URL'):
Jack Jansen705553a2003-05-06 12:44:00 +0000626 # For pseudo-packages that are already installed we don't
627 # return an error message
628 status, _ = self.installed()
629 if status == "yes":
630 return []
Tim Peters182b5ac2004-07-18 06:16:08 +0000631 return [(None,
Jack Jansenaf304a62005-01-03 15:44:18 +0000632 "Package %s cannot be installed automatically, see the description" %
Jack Jansen0ae32202003-04-09 13:25:43 +0000633 self.fullname())]
Jack Jansen5da131b2003-06-01 20:57:12 +0000634 if self.systemwideOnly() and self._db.preferences.isUserInstall():
635 return [(None,
Jack Jansenaf304a62005-01-03 15:44:18 +0000636 "Package %s can only be installed system-wide" %
Jack Jansen5da131b2003-06-01 20:57:12 +0000637 self.fullname())]
Jack Jansen0ae32202003-04-09 13:25:43 +0000638 if not self._dict.get('Prerequisites'):
639 return []
640 for item in self._dict['Prerequisites']:
641 if type(item) == str:
642 pkg = None
643 descr = str(item)
644 else:
645 name = item['Name']
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000646 if 'Version' in item:
Jack Jansen0ae32202003-04-09 13:25:43 +0000647 name = name + '-' + item['Version']
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000648 if 'Flavor' in item:
Jack Jansen0ae32202003-04-09 13:25:43 +0000649 name = name + '-' + item['Flavor']
650 pkg = self._db.find(name)
651 if not pkg:
652 descr = "Requires unknown %s"%name
653 else:
Jack Jansen2a97dcc2003-06-01 20:03:43 +0000654 descr = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +0000655 rv.append((pkg, descr))
656 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000657
658
Jack Jansen0ae32202003-04-09 13:25:43 +0000659 def downloadPackageOnly(self, output=None):
660 """Download a single package, if needed.
Tim Peters182b5ac2004-07-18 06:16:08 +0000661
Jack Jansen0ae32202003-04-09 13:25:43 +0000662 An MD5 signature is used to determine whether download is needed,
663 and to test that we actually downloaded what we expected.
664 If output is given it is a file-like object that will receive a log
665 of what happens.
Tim Peters182b5ac2004-07-18 06:16:08 +0000666
Jack Jansen0ae32202003-04-09 13:25:43 +0000667 If anything unforeseen happened the method returns an error message
668 string.
669 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000670
Jack Jansen0ae32202003-04-09 13:25:43 +0000671 scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
672 path = urllib.url2pathname(path)
673 filename = os.path.split(path)[1]
Tim Peters182b5ac2004-07-18 06:16:08 +0000674 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
Jack Jansen0ae32202003-04-09 13:25:43 +0000675 if not self._archiveOK():
676 if scheme == 'manual':
677 return "Please download package manually and save as %s" % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000678 downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000679 watcher=self._db.preferences.watcher)
680 if not downloader.download(self._dict['Download-URL'],
681 self.archiveFilename, output):
Jack Jansen0ae32202003-04-09 13:25:43 +0000682 return "download command failed"
683 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
684 return "archive not found after download"
685 if not self._archiveOK():
686 return "archive does not have correct MD5 checksum"
Tim Peters182b5ac2004-07-18 06:16:08 +0000687
Jack Jansen0ae32202003-04-09 13:25:43 +0000688 def _archiveOK(self):
689 """Test an archive. It should exist and the MD5 checksum should be correct."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000690
Jack Jansen0ae32202003-04-09 13:25:43 +0000691 if not os.path.exists(self.archiveFilename):
692 return 0
693 if not self._dict.get('MD5Sum'):
694 sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
695 return 1
696 data = open(self.archiveFilename, 'rb').read()
Thomas Wouters477c8d52006-05-27 19:21:47 +0000697 checksum = hashlib.md5(data).hexdigest()
Jack Jansen0ae32202003-04-09 13:25:43 +0000698 return checksum == self._dict['MD5Sum']
Tim Peters182b5ac2004-07-18 06:16:08 +0000699
Jack Jansen0ae32202003-04-09 13:25:43 +0000700 def unpackPackageOnly(self, output=None):
701 """Unpack a downloaded package archive."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000702
Jack Jansen0ae32202003-04-09 13:25:43 +0000703 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000704 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000705 if filename[-len(ext):] == ext:
706 break
707 else:
708 return "unknown extension for archive file: %s" % filename
709 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000710 unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir,
Jack Jansen989ddc02004-03-11 23:03:59 +0000711 watcher=self._db.preferences.watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000712 rv = unpacker.unpack(self.archiveFilename, output=output)
713 if rv:
714 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000715
Jack Jansen0ae32202003-04-09 13:25:43 +0000716 def installPackageOnly(self, output=None):
717 """Default install method, to be overridden by subclasses"""
718 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
719 % (self.fullname(), self._dict.get(flavor, ""))
Tim Peters182b5ac2004-07-18 06:16:08 +0000720
Jack Jansen0ae32202003-04-09 13:25:43 +0000721 def installSinglePackage(self, output=None):
722 """Download, unpack and install a single package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000723
Jack Jansen0ae32202003-04-09 13:25:43 +0000724 If output is given it should be a file-like object and it
725 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000726
Jack Jansen749f4812003-07-21 20:47:11 +0000727 if not self._dict.get('Download-URL'):
728 return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
Jack Jansen0ae32202003-04-09 13:25:43 +0000729 msg = self.downloadPackageOnly(output)
730 if msg:
731 return "%s: download: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000732
Jack Jansen0ae32202003-04-09 13:25:43 +0000733 msg = self.unpackPackageOnly(output)
734 if msg:
735 return "%s: unpack: %s" % (self.fullname(), msg)
Tim Peters182b5ac2004-07-18 06:16:08 +0000736
Jack Jansen0ae32202003-04-09 13:25:43 +0000737 return self.installPackageOnly(output)
Tim Peters182b5ac2004-07-18 06:16:08 +0000738
Jack Jansen0ae32202003-04-09 13:25:43 +0000739 def beforeInstall(self):
740 """Bookkeeping before installation: remember what we have in site-packages"""
741 self._old_contents = os.listdir(self._db.preferences.installDir)
Tim Peters182b5ac2004-07-18 06:16:08 +0000742
Jack Jansen0ae32202003-04-09 13:25:43 +0000743 def afterInstall(self):
744 """Bookkeeping after installation: interpret any new .pth files that have
745 appeared"""
Tim Peters182b5ac2004-07-18 06:16:08 +0000746
Jack Jansen0ae32202003-04-09 13:25:43 +0000747 new_contents = os.listdir(self._db.preferences.installDir)
748 for fn in new_contents:
749 if fn in self._old_contents:
750 continue
751 if fn[-4:] != '.pth':
752 continue
753 fullname = os.path.join(self._db.preferences.installDir, fn)
754 f = open(fullname)
755 for line in f.readlines():
756 if not line:
757 continue
758 if line[0] == '#':
759 continue
760 if line[:6] == 'import':
Georg Brandl7cae87c2006-09-06 06:51:57 +0000761 exec(line)
Jack Jansen0ae32202003-04-09 13:25:43 +0000762 continue
763 if line[-1] == '\n':
764 line = line[:-1]
765 if not os.path.isabs(line):
766 line = os.path.join(self._db.preferences.installDir, line)
767 line = os.path.realpath(line)
768 if not line in sys.path:
Tim Peters182b5ac2004-07-18 06:16:08 +0000769 sys.path.append(line)
Jack Jansen95839b82003-02-09 23:10:20 +0000770
Jack Jansen5da131b2003-06-01 20:57:12 +0000771 def filterExpectedSkips(self, names):
772 """Return a list that contains only unpexpected skips"""
773 if not self._db.preferences.isUserInstall():
774 return names
775 expected_skips = self._dict.get('User-install-skips')
776 if not expected_skips:
777 return names
778 newnames = []
779 for name in names:
780 for skip in expected_skips:
781 if name[:len(skip)] == skip:
782 break
783 else:
784 newnames.append(name)
785 return newnames
786
Jack Jansen0dacac42003-02-14 14:11:59 +0000787class PimpPackage_binary(PimpPackage):
788
Jack Jansen0ae32202003-04-09 13:25:43 +0000789 def unpackPackageOnly(self, output=None):
790 """We don't unpack binary packages until installing"""
791 pass
Tim Peters182b5ac2004-07-18 06:16:08 +0000792
Jack Jansen0ae32202003-04-09 13:25:43 +0000793 def installPackageOnly(self, output=None):
794 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000795
Jack Jansen0ae32202003-04-09 13:25:43 +0000796 If output is given it should be a file-like object and it
797 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000798
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000799 if 'Install-command' in self._dict:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000800 return "%s: Binary package cannot have Install-command" % self.fullname()
Tim Peters182b5ac2004-07-18 06:16:08 +0000801
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000802 if 'Pre-install-command' in self._dict:
Jack Jansenaf304a62005-01-03 15:44:18 +0000803 if _cmd(output, '/tmp', self._dict['Pre-install-command']):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000804 return "pre-install %s: running \"%s\" failed" % \
805 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000806
Jack Jansen0ae32202003-04-09 13:25:43 +0000807 self.beforeInstall()
Jack Jansen0dacac42003-02-14 14:11:59 +0000808
Jack Jansen0ae32202003-04-09 13:25:43 +0000809 # Install by unpacking
810 filename = os.path.split(self.archiveFilename)[1]
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000811 for ext, unpackerClass, arg in ARCHIVE_FORMATS:
Jack Jansen0ae32202003-04-09 13:25:43 +0000812 if filename[-len(ext):] == ext:
813 break
814 else:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000815 return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
816 self.basename = filename[:-len(ext)]
Tim Peters182b5ac2004-07-18 06:16:08 +0000817
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000818 install_renames = []
819 for k, newloc in self._db.preferences.installLocations:
820 if not newloc:
821 continue
822 if k == "--install-lib":
823 oldloc = DEFAULT_INSTALLDIR
824 else:
825 return "%s: Don't know installLocation %s" % (self.fullname(), k)
826 install_renames.append((oldloc, newloc))
Tim Peters182b5ac2004-07-18 06:16:08 +0000827
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000828 unpacker = unpackerClass(arg, dir="/", renames=install_renames)
Jack Jansen5da131b2003-06-01 20:57:12 +0000829 rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000830 if rv:
831 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000832
Jack Jansen0ae32202003-04-09 13:25:43 +0000833 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000834
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000835 if 'Post-install-command' in self._dict:
Jack Jansenaf304a62005-01-03 15:44:18 +0000836 if _cmd(output, '/tmp', self._dict['Post-install-command']):
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000837 return "%s: post-install: running \"%s\" failed" % \
Jack Jansen0ae32202003-04-09 13:25:43 +0000838 (self.fullname(), self._dict['Post-install-command'])
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000839
Jack Jansen0ae32202003-04-09 13:25:43 +0000840 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000841
842
Jack Jansen0dacac42003-02-14 14:11:59 +0000843class PimpPackage_source(PimpPackage):
844
Jack Jansen0ae32202003-04-09 13:25:43 +0000845 def unpackPackageOnly(self, output=None):
846 """Unpack a source package and check that setup.py exists"""
847 PimpPackage.unpackPackageOnly(self, output)
848 # Test that a setup script has been create
849 self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
850 setupname = os.path.join(self._buildDirname, "setup.py")
851 if not os.path.exists(setupname) and not NO_EXECUTE:
852 return "no setup.py found after unpack of archive"
Jack Jansen0dacac42003-02-14 14:11:59 +0000853
Jack Jansen0ae32202003-04-09 13:25:43 +0000854 def installPackageOnly(self, output=None):
855 """Install a single source package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000856
Jack Jansen0ae32202003-04-09 13:25:43 +0000857 If output is given it should be a file-like object and it
858 will receive a log of what happened."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000859
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000860 if 'Pre-install-command' in self._dict:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000861 if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000862 return "pre-install %s: running \"%s\" failed" % \
863 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters182b5ac2004-07-18 06:16:08 +0000864
Jack Jansen0ae32202003-04-09 13:25:43 +0000865 self.beforeInstall()
866 installcmd = self._dict.get('Install-command')
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000867 if installcmd and self._install_renames:
868 return "Package has install-command and can only be installed to standard location"
869 # This is the "bit-bucket" for installations: everything we don't
870 # want. After installation we check that it is actually empty
871 unwanted_install_dir = None
Jack Jansen0ae32202003-04-09 13:25:43 +0000872 if not installcmd:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000873 extra_args = ""
874 for k, v in self._db.preferences.installLocations:
875 if not v:
876 # We don't want these files installed. Send them
877 # to the bit-bucket.
878 if not unwanted_install_dir:
879 unwanted_install_dir = tempfile.mkdtemp()
880 v = unwanted_install_dir
881 extra_args = extra_args + " %s \"%s\"" % (k, v)
882 installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
883 if _cmd(output, self._buildDirname, installcmd):
Jack Jansen0ae32202003-04-09 13:25:43 +0000884 return "install %s: running \"%s\" failed" % \
885 (self.fullname(), installcmd)
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000886 if unwanted_install_dir and os.path.exists(unwanted_install_dir):
887 unwanted_files = os.listdir(unwanted_install_dir)
888 if unwanted_files:
889 rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
890 else:
891 rv = None
892 shutil.rmtree(unwanted_install_dir)
893 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000894
Jack Jansen0ae32202003-04-09 13:25:43 +0000895 self.afterInstall()
Tim Peters182b5ac2004-07-18 06:16:08 +0000896
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000897 if 'Post-install-command' in self._dict:
Jack Jansen6fde1ce2003-04-15 14:43:05 +0000898 if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
Jack Jansen0ae32202003-04-09 13:25:43 +0000899 return "post-install %s: running \"%s\" failed" % \
900 (self.fullname(), self._dict['Post-install-command'])
901 return None
Tim Peters182b5ac2004-07-18 06:16:08 +0000902
Jack Jansenaf304a62005-01-03 15:44:18 +0000903class PimpPackage_installer(PimpPackage):
904
905 def unpackPackageOnly(self, output=None):
906 """We don't unpack dmg packages until installing"""
907 pass
908
909 def installPackageOnly(self, output=None):
910 """Install a single source package.
Tim Peters5a9fb3c2005-01-07 16:01:32 +0000911
Jack Jansenaf304a62005-01-03 15:44:18 +0000912 If output is given it should be a file-like object and it
913 will receive a log of what happened."""
Tim Peters5a9fb3c2005-01-07 16:01:32 +0000914
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000915 if 'Post-install-command' in self._dict:
Jack Jansenaf304a62005-01-03 15:44:18 +0000916 return "%s: Installer package cannot have Post-install-command" % self.fullname()
917
Neal Norwitzf1a69c12006-08-20 16:25:10 +0000918 if 'Pre-install-command' in self._dict:
Jack Jansenaf304a62005-01-03 15:44:18 +0000919 if _cmd(output, '/tmp', self._dict['Pre-install-command']):
920 return "pre-install %s: running \"%s\" failed" % \
921 (self.fullname(), self._dict['Pre-install-command'])
Tim Peters5a9fb3c2005-01-07 16:01:32 +0000922
Jack Jansenaf304a62005-01-03 15:44:18 +0000923 self.beforeInstall()
924
925 installcmd = self._dict.get('Install-command')
926 if installcmd:
927 if '%' in installcmd:
928 installcmd = installcmd % self.archiveFilename
929 else:
Tim Peters5a9fb3c2005-01-07 16:01:32 +0000930 installcmd = 'open \"%s\"' % self.archiveFilename
Jack Jansenaf304a62005-01-03 15:44:18 +0000931 if _cmd(output, "/tmp", installcmd):
932 return '%s: install command failed (use verbose for details)' % self.fullname()
933 return '%s: downloaded and opened. Install manually and restart Package Manager' % self.archiveFilename
Tim Peters182b5ac2004-07-18 06:16:08 +0000934
Jack Jansen95839b82003-02-09 23:10:20 +0000935class PimpInstaller:
Jack Jansen0ae32202003-04-09 13:25:43 +0000936 """Installer engine: computes dependencies and installs
937 packages in the right order."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000938
Jack Jansen0ae32202003-04-09 13:25:43 +0000939 def __init__(self, db):
940 self._todo = []
941 self._db = db
942 self._curtodo = []
943 self._curmessages = []
Tim Peters182b5ac2004-07-18 06:16:08 +0000944
Jack Jansen0ae32202003-04-09 13:25:43 +0000945 def __contains__(self, package):
946 return package in self._todo
Tim Peters182b5ac2004-07-18 06:16:08 +0000947
Jack Jansen0ae32202003-04-09 13:25:43 +0000948 def _addPackages(self, packages):
949 for package in packages:
950 if not package in self._todo:
Jack Jansen118e1272004-03-13 23:50:48 +0000951 self._todo.append(package)
Tim Peters182b5ac2004-07-18 06:16:08 +0000952
Jack Jansen0ae32202003-04-09 13:25:43 +0000953 def _prepareInstall(self, package, force=0, recursive=1):
954 """Internal routine, recursive engine for prepareInstall.
Tim Peters182b5ac2004-07-18 06:16:08 +0000955
Jack Jansen0ae32202003-04-09 13:25:43 +0000956 Test whether the package is installed and (if not installed
957 or if force==1) prepend it to the temporary todo list and
958 call ourselves recursively on all prerequisites."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000959
Jack Jansen0ae32202003-04-09 13:25:43 +0000960 if not force:
961 status, message = package.installed()
962 if status == "yes":
Tim Peters182b5ac2004-07-18 06:16:08 +0000963 return
Jack Jansen0ae32202003-04-09 13:25:43 +0000964 if package in self._todo or package in self._curtodo:
965 return
966 self._curtodo.insert(0, package)
967 if not recursive:
968 return
969 prereqs = package.prerequisites()
970 for pkg, descr in prereqs:
971 if pkg:
Jack Jansen0576d0a2004-03-13 23:03:38 +0000972 self._prepareInstall(pkg, False, recursive)
Jack Jansen0ae32202003-04-09 13:25:43 +0000973 else:
Jack Jansen20fa6752003-04-16 12:15:34 +0000974 self._curmessages.append("Problem with dependency: %s" % descr)
Tim Peters182b5ac2004-07-18 06:16:08 +0000975
Jack Jansen0ae32202003-04-09 13:25:43 +0000976 def prepareInstall(self, package, force=0, recursive=1):
977 """Prepare installation of a package.
Tim Peters182b5ac2004-07-18 06:16:08 +0000978
Jack Jansen0ae32202003-04-09 13:25:43 +0000979 If the package is already installed and force is false nothing
980 is done. If recursive is true prerequisites are installed first.
Tim Peters182b5ac2004-07-18 06:16:08 +0000981
Jack Jansen0ae32202003-04-09 13:25:43 +0000982 Returns a list of packages (to be passed to install) and a list
983 of messages of any problems encountered.
984 """
Tim Peters182b5ac2004-07-18 06:16:08 +0000985
Jack Jansen0ae32202003-04-09 13:25:43 +0000986 self._curtodo = []
987 self._curmessages = []
988 self._prepareInstall(package, force, recursive)
989 rv = self._curtodo, self._curmessages
990 self._curtodo = []
991 self._curmessages = []
992 return rv
Tim Peters182b5ac2004-07-18 06:16:08 +0000993
Jack Jansen0ae32202003-04-09 13:25:43 +0000994 def install(self, packages, output):
995 """Install a list of packages."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000996
Jack Jansen0ae32202003-04-09 13:25:43 +0000997 self._addPackages(packages)
998 status = []
999 for pkg in self._todo:
1000 msg = pkg.installSinglePackage(output)
1001 if msg:
1002 status.append(msg)
1003 return status
Tim Peters182b5ac2004-07-18 06:16:08 +00001004
1005
1006
Jack Jansen989ddc02004-03-11 23:03:59 +00001007def _run(mode, verbose, force, args, prefargs, watcher):
Jack Jansen0ae32202003-04-09 13:25:43 +00001008 """Engine for the main program"""
Tim Peters182b5ac2004-07-18 06:16:08 +00001009
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001010 prefs = PimpPreferences(**prefargs)
Jack Jansen989ddc02004-03-11 23:03:59 +00001011 if watcher:
1012 prefs.setWatcher(watcher)
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001013 rv = prefs.check()
1014 if rv:
1015 sys.stdout.write(rv)
Jack Jansen0ae32202003-04-09 13:25:43 +00001016 db = PimpDatabase(prefs)
1017 db.appendURL(prefs.pimpDatabase)
Tim Peters182b5ac2004-07-18 06:16:08 +00001018
Jack Jansen0ae32202003-04-09 13:25:43 +00001019 if mode == 'dump':
1020 db.dump(sys.stdout)
1021 elif mode =='list':
1022 if not args:
1023 args = db.listnames()
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001024 print("%-20.20s\t%s" % ("Package", "Description"))
1025 print()
Jack Jansen0ae32202003-04-09 13:25:43 +00001026 for pkgname in args:
1027 pkg = db.find(pkgname)
1028 if pkg:
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001029 description = pkg.shortdescription()
Jack Jansen0ae32202003-04-09 13:25:43 +00001030 pkgname = pkg.fullname()
1031 else:
1032 description = 'Error: no such package'
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001033 print("%-20.20s\t%s" % (pkgname, description))
Jack Jansen0ae32202003-04-09 13:25:43 +00001034 if verbose:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001035 print("\tHome page:\t", pkg.homepage())
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001036 try:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001037 print("\tDownload URL:\t", pkg.downloadURL())
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001038 except KeyError:
1039 pass
Jack Jansen9f0c5752003-05-29 22:07:27 +00001040 description = pkg.description()
Jack Jansen2a97dcc2003-06-01 20:03:43 +00001041 description = '\n\t\t\t\t\t'.join(description.splitlines())
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001042 print("\tDescription:\t%s" % description)
Jack Jansen0ae32202003-04-09 13:25:43 +00001043 elif mode =='status':
1044 if not args:
1045 args = db.listnames()
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001046 print("%-20.20s\t%s\t%s" % ("Package", "Installed", "Message"))
1047 print()
Jack Jansen0ae32202003-04-09 13:25:43 +00001048 for pkgname in args:
1049 pkg = db.find(pkgname)
1050 if pkg:
1051 status, msg = pkg.installed()
1052 pkgname = pkg.fullname()
1053 else:
1054 status = 'error'
1055 msg = 'No such package'
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001056 print("%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg))
Jack Jansen0ae32202003-04-09 13:25:43 +00001057 if verbose and status == "no":
1058 prereq = pkg.prerequisites()
1059 for pkg, msg in prereq:
1060 if not pkg:
1061 pkg = ''
1062 else:
1063 pkg = pkg.fullname()
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001064 print("%-20.20s\tRequirement: %s %s" % ("", pkg, msg))
Jack Jansen0ae32202003-04-09 13:25:43 +00001065 elif mode == 'install':
1066 if not args:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001067 print('Please specify packages to install')
Jack Jansen0ae32202003-04-09 13:25:43 +00001068 sys.exit(1)
1069 inst = PimpInstaller(db)
1070 for pkgname in args:
1071 pkg = db.find(pkgname)
1072 if not pkg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001073 print('%s: No such package' % pkgname)
Jack Jansen0ae32202003-04-09 13:25:43 +00001074 continue
1075 list, messages = inst.prepareInstall(pkg, force)
1076 if messages and not force:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001077 print("%s: Not installed:" % pkgname)
Jack Jansen0ae32202003-04-09 13:25:43 +00001078 for m in messages:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001079 print("\t", m)
Jack Jansen0ae32202003-04-09 13:25:43 +00001080 else:
1081 if verbose:
1082 output = sys.stdout
1083 else:
1084 output = None
1085 messages = inst.install(list, output)
1086 if messages:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001087 print("%s: Not installed:" % pkgname)
Jack Jansen0ae32202003-04-09 13:25:43 +00001088 for m in messages:
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001089 print("\t", m)
Jack Jansen95839b82003-02-09 23:10:20 +00001090
1091def main():
Jack Jansen0ae32202003-04-09 13:25:43 +00001092 """Minimal commandline tool to drive pimp."""
Tim Peters182b5ac2004-07-18 06:16:08 +00001093
Jack Jansen0ae32202003-04-09 13:25:43 +00001094 import getopt
1095 def _help():
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001096 print("Usage: pimp [options] -s [package ...] List installed status")
1097 print(" pimp [options] -l [package ...] Show package information")
1098 print(" pimp [options] -i package ... Install packages")
1099 print(" pimp -d Dump database to stdout")
1100 print(" pimp -V Print version number")
1101 print("Options:")
1102 print(" -v Verbose")
1103 print(" -f Force installation")
1104 print(" -D dir Set destination directory")
1105 print(" (default: %s)" % DEFAULT_INSTALLDIR)
1106 print(" -u url URL for database")
Jack Jansen0ae32202003-04-09 13:25:43 +00001107 sys.exit(1)
Tim Peters182b5ac2004-07-18 06:16:08 +00001108
Jack Jansen989ddc02004-03-11 23:03:59 +00001109 class _Watcher:
1110 def update(self, msg):
1111 sys.stderr.write(msg + '\r')
1112 return 1
Tim Peters182b5ac2004-07-18 06:16:08 +00001113
Jack Jansen0ae32202003-04-09 13:25:43 +00001114 try:
Jack Jansenb789a062003-05-28 18:56:30 +00001115 opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
1116 except getopt.GetoptError:
Jack Jansen0ae32202003-04-09 13:25:43 +00001117 _help()
1118 if not opts and not args:
1119 _help()
1120 mode = None
1121 force = 0
1122 verbose = 0
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001123 prefargs = {}
Jack Jansen989ddc02004-03-11 23:03:59 +00001124 watcher = None
Jack Jansen0ae32202003-04-09 13:25:43 +00001125 for o, a in opts:
1126 if o == '-s':
1127 if mode:
1128 _help()
1129 mode = 'status'
1130 if o == '-l':
1131 if mode:
1132 _help()
1133 mode = 'list'
1134 if o == '-d':
1135 if mode:
1136 _help()
1137 mode = 'dump'
Jack Jansenb789a062003-05-28 18:56:30 +00001138 if o == '-V':
1139 if mode:
1140 _help()
1141 mode = 'version'
Jack Jansen0ae32202003-04-09 13:25:43 +00001142 if o == '-i':
1143 mode = 'install'
1144 if o == '-f':
1145 force = 1
1146 if o == '-v':
1147 verbose = 1
Jack Jansen989ddc02004-03-11 23:03:59 +00001148 watcher = _Watcher()
Jack Jansen6fde1ce2003-04-15 14:43:05 +00001149 if o == '-D':
1150 prefargs['installDir'] = a
Jack Jansenb789a062003-05-28 18:56:30 +00001151 if o == '-u':
1152 prefargs['pimpDatabase'] = a
Jack Jansen0ae32202003-04-09 13:25:43 +00001153 if not mode:
1154 _help()
Jack Jansenb789a062003-05-28 18:56:30 +00001155 if mode == 'version':
Guido van Rossumbe19ed72007-02-09 05:37:30 +00001156 print('Pimp version %s; module name is %s' % (PIMP_VERSION, __name__))
Jack Jansenb789a062003-05-28 18:56:30 +00001157 else:
Jack Jansen989ddc02004-03-11 23:03:59 +00001158 _run(mode, verbose, force, args, prefargs, watcher)
Jack Jansenb789a062003-05-28 18:56:30 +00001159
1160# Finally, try to update ourselves to a newer version.
1161# If the end-user updates pimp through pimp the new version
1162# will be called pimp_update and live in site-packages
1163# or somewhere similar
1164if __name__ != 'pimp_update':
1165 try:
1166 import pimp_update
1167 except ImportError:
1168 pass
1169 else:
1170 if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1171 import warnings
1172 warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1173 (pimp_update.PIMP_VERSION, PIMP_VERSION))
1174 else:
1175 from pimp_update import *
Tim Peters182b5ac2004-07-18 06:16:08 +00001176
Jack Jansen95839b82003-02-09 23:10:20 +00001177if __name__ == '__main__':
Jack Jansen0ae32202003-04-09 13:25:43 +00001178 main()