blob: 5876d8655883dde376d379189837b105b2bf8b34 [file] [log] [blame]
Jack Jansen95839b82003-02-09 23:10:20 +00001import sys
2import os
3import urllib
4import urlparse
5import plistlib
6import distutils.util
7
8_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
9_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
10_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
11
12NO_EXECUTE=0
13
14DEFAULT_FLAVORORDER=['source', 'binary']
15DEFAULT_DOWNLOADDIR='/tmp'
16DEFAULT_BUILDDIR='/tmp'
17DEFAULT_INSTALLDIR=os.path.join(sys.prefix, "Lib", "site-packages")
18DEFAULT_PIMPDATABASE="http://www.cwi.nl/~jack/pimp/pimp-%s.plist" % distutils.util.get_platform()
19
20ARCHIVE_FORMATS = [
21 (".tar.Z", "zcat \"%s\" | tar xf -"),
22 (".taz", "zcat \"%s\" | tar xf -"),
23 (".tar.gz", "zcat \"%s\" | tar xf -"),
24 (".tgz", "zcat \"%s\" | tar xf -"),
25 (".tar.bz", "bzcat \"%s\" | tar xf -"),
26]
27
28class PimpPreferences:
29 def __init__(self,
30 flavorOrder=None,
31 downloadDir=None,
32 buildDir=None,
33 installDir=None,
34 pimpDatabase=None):
35 if not flavorOrder:
36 flavorOrder = DEFAULT_FLAVORORDER
37 if not downloadDir:
38 downloadDir = DEFAULT_DOWNLOADDIR
39 if not buildDir:
40 buildDir = DEFAULT_BUILDDIR
41 if not installDir:
42 installDir = DEFAULT_INSTALLDIR
43 if not pimpDatabase:
44 pimpDatabase = DEFAULT_PIMPDATABASE
45 self.flavorOrder = flavorOrder
46 self.downloadDir = downloadDir
47 self.buildDir = buildDir
48 self.installDir = installDir
49 self.pimpDatabase = pimpDatabase
50
51 def check(self):
52 rv = ""
53 RWX_OK = os.R_OK|os.W_OK|os.X_OK
54 if not os.path.exists(self.downloadDir):
55 rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
56 elif not os.access(self.downloadDir, RWX_OK):
57 rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
58 if not os.path.exists(self.buildDir):
59 rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
60 elif not os.access(self.buildDir, RWX_OK):
61 rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
62 if not os.path.exists(self.installDir):
63 rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
64 elif not os.access(self.installDir, RWX_OK):
65 rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
66 else:
67 installDir = os.path.realpath(self.installDir)
68 for p in sys.path:
69 try:
70 realpath = os.path.realpath(p)
71 except:
72 pass
73 if installDir == realpath:
74 break
75 else:
76 rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
77 return rv
78
79 def compareFlavors(self, left, right):
80 if left in self.flavorOrder:
81 if right in self.flavorOrder:
82 return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
83 return -1
84 if right in self.flavorOrder:
85 return 1
86 return cmp(left, right)
87
88class PimpDatabase:
89 def __init__(self, prefs):
90 self._packages = []
91 self.preferences = prefs
92 self._urllist = []
93 self._version = ""
94 self._maintainer = ""
95 self._description = ""
96
97 def appendURL(self, url, included=0):
98 if url in self._urllist:
99 return
100 self._urllist.append(url)
101 fp = urllib.urlopen(url).fp
102 dict = plistlib.Plist.fromFile(fp)
103 # Test here for Pimp version, etc
104 if not included:
105 self._version = dict.get('version', '0.1')
106 self._maintainer = dict.get('maintainer', '')
107 self._description = dict.get('description', '')
108 self.appendPackages(dict['packages'])
109 others = dict.get('include', [])
110 for url in others:
111 self.appendURL(url, included=1)
112
113 def appendPackages(self, packages):
114 for p in packages:
115 pkg = PimpPackage(self, **dict(p))
116 self._packages.append(pkg)
117
118 def list(self):
119 return self._packages
120
121 def listnames(self):
122 rv = []
123 for pkg in self._packages:
124 rv.append(_fmtpackagename(pkg))
125 return rv
126
127 def dump(self, pathOrFile):
128 packages = []
129 for pkg in self._packages:
130 packages.append(pkg.dump())
131 dict = {
132 'version': self._version,
133 'maintainer': self._maintainer,
134 'description': self._description,
135 'packages': packages
136 }
137 plist = plistlib.Plist(**dict)
138 plist.write(pathOrFile)
139
140 def find(self, ident):
141 if type(ident) == str:
142 # Remove ( and ) for pseudo-packages
143 if ident[0] == '(' and ident[-1] == ')':
144 ident = ident[1:-1]
145 # Split into name-version-flavor
146 fields = ident.split('-')
147 if len(fields) < 1 or len(fields) > 3:
148 return None
149 name = fields[0]
150 if len(fields) > 1:
151 version = fields[1]
152 else:
153 version = None
154 if len(fields) > 2:
155 flavor = fields[2]
156 else:
157 flavor = None
158 else:
159 name = ident['name']
160 version = ident.get('version')
161 flavor = ident.get('flavor')
162 found = None
163 for p in self._packages:
164 if name == p.name and \
165 (not version or version == p.version) and \
166 (not flavor or flavor == p.flavor):
167 if not found or found < p:
168 found = p
169 return found
170
171class PimpPackage:
172 def __init__(self, db, name,
173 version=None,
174 flavor=None,
175 description=None,
176 longdesc=None,
177 downloadURL=None,
178 installTest=None,
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000179 prerequisites=None,
180 preInstall=None,
181 postInstall=None):
Jack Jansen95839b82003-02-09 23:10:20 +0000182 self._db = db
183 self.name = name
184 self.version = version
185 self.flavor = flavor
186 self.description = description
187 self.longdesc = longdesc
188 self.downloadURL = downloadURL
189 self._installTest = installTest
190 self._prerequisites = prerequisites
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000191 self._preInstall = preInstall
192 self._postInstall = postInstall
Jack Jansen95839b82003-02-09 23:10:20 +0000193
194 def dump(self):
195 dict = {
196 'name': self.name,
197 }
198 if self.version:
199 dict['version'] = self.version
200 if self.flavor:
201 dict['flavor'] = self.flavor
202 if self.description:
203 dict['description'] = self.description
204 if self.longdesc:
205 dict['longdesc'] = self.longdesc
206 if self.downloadURL:
207 dict['downloadURL'] = self.downloadURL
208 if self._installTest:
209 dict['installTest'] = self._installTest
210 if self._prerequisites:
211 dict['prerequisites'] = self._prerequisites
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000212 if self._preInstall:
213 dict['preInstall'] = self._preInstall
214 if self._postInstall:
215 dict['postInstall'] = self._postInstall
Jack Jansen95839b82003-02-09 23:10:20 +0000216 return dict
217
218 def __cmp__(self, other):
219 if not isinstance(other, PimpPackage):
220 return cmp(id(self), id(other))
221 if self.name != other.name:
222 return cmp(self.name, other.name)
223 if self.version != other.version:
224 return cmp(self.version, other.version)
225 return self._db.preferences.compareFlavors(self.flavor, other.flavor)
226
227 def installed(self):
228 namespace = {
229 "NotInstalled": _scriptExc_NotInstalled,
230 "OldInstalled": _scriptExc_OldInstalled,
231 "BadInstalled": _scriptExc_BadInstalled,
232 "os": os,
233 "sys": sys,
234 }
235 installTest = self._installTest.strip() + '\n'
236 try:
237 exec installTest in namespace
238 except ImportError, arg:
239 return "no", str(arg)
240 except _scriptExc_NotInstalled, arg:
241 return "no", str(arg)
242 except _scriptExc_OldInstalled, arg:
243 return "old", str(arg)
244 except _scriptExc_BadInstalled, arg:
245 return "bad", str(arg)
246 except:
247 print 'TEST:', repr(self._installTest)
248 return "bad", "Package install test got exception"
249 return "yes", ""
250
251 def prerequisites(self):
252 rv = []
253 if not self.downloadURL:
254 return [(None, "This package needs to be installed manually")]
255 if not self._prerequisites:
256 return []
257 for item in self._prerequisites:
258 if type(item) == str:
259 pkg = None
260 descr = str(item)
261 else:
262 pkg = self._db.find(item)
263 if not pkg:
264 descr = "Requires unknown %s"%_fmtpackagename(item)
265 else:
266 descr = pkg.description
267 rv.append((pkg, descr))
268 return rv
269
270 def _cmd(self, output, dir, *cmditems):
271 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
272 if output:
273 output.write("+ %s\n" % cmd)
274 if NO_EXECUTE:
275 return 0
276 fp = os.popen(cmd, "r")
277 while 1:
278 line = fp.readline()
279 if not line:
280 break
281 if output:
282 output.write(line)
283 rv = fp.close()
284 return rv
285
286 def downloadSinglePackage(self, output):
287 scheme, loc, path, query, frag = urlparse.urlsplit(self.downloadURL)
288 path = urllib.url2pathname(path)
289 filename = os.path.split(path)[1]
290 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
291 if self._cmd(output, self._db.preferences.downloadDir, "curl",
292 "--output", self.archiveFilename,
293 self.downloadURL):
294 return "download command failed"
295 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
296 return "archive not found after download"
297
298 def unpackSinglePackage(self, output):
299 filename = os.path.split(self.archiveFilename)[1]
300 for ext, cmd in ARCHIVE_FORMATS:
301 if filename[-len(ext):] == ext:
302 break
303 else:
304 return "unknown extension for archive file: %s" % filename
305 basename = filename[:-len(ext)]
306 cmd = cmd % self.archiveFilename
307 self._buildDirname = os.path.join(self._db.preferences.buildDir, basename)
308 if self._cmd(output, self._db.preferences.buildDir, cmd):
309 return "unpack command failed"
310 setupname = os.path.join(self._buildDirname, "setup.py")
311 if not os.path.exists(setupname) and not NO_EXECUTE:
312 return "no setup.py found after unpack of archive"
313
314 def installSinglePackage(self, output):
315 if not self.downloadURL:
316 return "%s: This package needs to be installed manually" % _fmtpackagename(self)
317 msg = self.downloadSinglePackage(output)
318 if msg:
319 return "download %s: %s" % (_fmtpackagename(self), msg)
320 msg = self.unpackSinglePackage(output)
321 if msg:
322 return "unpack %s: %s" % (_fmtpackagename(self), msg)
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000323 if self._preInstall:
324 if self._cmd(output, self._buildDirname, self._preInstall):
325 return "pre-install %s: running \"%s\" failed" % \
326 (_fmtpackagename(self), self._preInstall)
Jack Jansen95839b82003-02-09 23:10:20 +0000327 if self._cmd(output, self._buildDirname, sys.executable, "setup.py install"):
328 return "install %s: running \"setup.py install\" failed" % _fmtpackagename(self)
Jack Jansenb4bb64e2003-02-10 13:08:04 +0000329 if self._postInstall:
330 if self._cmd(output, self._buildDirname, self._postInstall):
331 return "post-install %s: running \"%s\" failed" % \
332 (_fmtpackagename(self), self._postInstall)
Jack Jansen95839b82003-02-09 23:10:20 +0000333 return None
334
335class PimpInstaller:
336 def __init__(self, db):
337 self._todo = []
338 self._db = db
339 self._curtodo = []
340 self._curmessages = []
341
342 def __contains__(self, package):
343 return package in self._todo
344
345 def _addPackages(self, packages):
346 for package in packages:
347 if not package in self._todo:
348 self._todo.insert(0, package)
349
350 def _prepareInstall(self, package, force=0, recursive=1):
351 if not force:
352 status, message = package.installed()
353 if status == "yes":
354 return
355 if package in self._todo or package in self._curtodo:
356 return
357 self._curtodo.insert(0, package)
358 if not recursive:
359 return
360 prereqs = package.prerequisites()
361 for pkg, descr in prereqs:
362 if pkg:
363 self._prepareInstall(pkg, force, recursive)
364 else:
365 self._curmessages.append("Requires: %s" % descr)
366
367 def prepareInstall(self, package, force=0, recursive=1):
368 self._curtodo = []
369 self._curmessages = []
370 self._prepareInstall(package, force, recursive)
371 rv = self._curtodo, self._curmessages
372 self._curtodo = []
373 self._curmessages = []
374 return rv
375
376 def install(self, packages, output):
377 self._addPackages(packages)
378 status = []
379 for pkg in self._todo:
380 msg = pkg.installSinglePackage(output)
381 if msg:
382 status.append(msg)
383 return status
384
385
386def _fmtpackagename(dict):
387 if isinstance(dict, PimpPackage):
388 dict = dict.dump()
389 rv = dict['name']
390 if dict.has_key('version'):
391 rv = rv + '-%s' % dict['version']
392 if dict.has_key('flavor'):
393 rv = rv + '-%s' % dict['flavor']
394 if not dict.get('downloadURL'):
395 # Pseudo-package, show in parentheses
396 rv = '(%s)' % rv
397 return rv
398
399def _run(mode, verbose, force, args):
400 prefs = PimpPreferences()
401 prefs.check()
402 db = PimpDatabase(prefs)
403 db.appendURL(prefs.pimpDatabase)
404
405 if mode =='list':
406 if not args:
407 args = db.listnames()
408 print "%-20.20s\t%s" % ("Package", "Description")
409 print
410 for pkgname in args:
411 pkg = db.find(pkgname)
412 if pkg:
413 description = pkg.description
414 pkgname = _fmtpackagename(pkg)
415 else:
416 description = 'Error: no such package'
417 print "%-20.20s\t%s" % (pkgname, description)
418 if verbose:
419 print "\tHome page:\t", pkg.longdesc
420 print "\tDownload URL:\t", pkg.downloadURL
421 if mode =='status':
422 if not args:
423 args = db.listnames()
424 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
425 print
426 for pkgname in args:
427 pkg = db.find(pkgname)
428 if pkg:
429 status, msg = pkg.installed()
430 pkgname = _fmtpackagename(pkg)
431 else:
432 status = 'error'
433 msg = 'No such package'
434 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
435 if verbose and status == "no":
436 prereq = pkg.prerequisites()
437 for pkg, msg in prereq:
438 if not pkg:
439 pkg = ''
440 else:
441 pkg = _fmtpackagename(pkg)
442 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
443 elif mode == 'install':
444 if not args:
445 print 'Please specify packages to install'
446 sys.exit(1)
447 inst = PimpInstaller(db)
448 for pkgname in args:
449 pkg = db.find(pkgname)
450 if not pkg:
451 print '%s: No such package' % pkgname
452 continue
453 list, messages = inst.prepareInstall(pkg, force)
454 if messages and not force:
455 print "%s: Not installed:" % pkgname
456 for m in messages:
457 print "\t", m
458 else:
459 if verbose:
460 output = sys.stdout
461 else:
462 output = None
463 messages = inst.install(list, output)
464 if messages:
465 print "%s: Not installed:" % pkgname
466 for m in messages:
467 print "\t", m
468
469def main():
470 import getopt
471 def _help():
472 print "Usage: pimp [-v] -s [package ...] List installed status"
473 print " pimp [-v] -l [package ...] Show package information"
474 print " pimp [-vf] -i package ... Install packages"
475 print "Options:"
476 print " -v Verbose"
477 print " -f Force installation"
478 sys.exit(1)
479
480 try:
481 opts, args = getopt.getopt(sys.argv[1:], "slifv")
482 except getopt.Error:
483 _help()
484 if not opts and not args:
485 _help()
486 mode = None
487 force = 0
488 verbose = 0
489 for o, a in opts:
490 if o == '-s':
491 if mode:
492 _help()
493 mode = 'status'
494 if o == '-l':
495 if mode:
496 _help()
497 mode = 'list'
498 if o == '-L':
499 if mode:
500 _help()
501 mode = 'longlist'
502 if o == '-i':
503 mode = 'install'
504 if o == '-f':
505 force = 1
506 if o == '-v':
507 verbose = 1
508 if not mode:
509 _help()
510 _run(mode, verbose, force, args)
511
512if __name__ == '__main__':
513 main()
514
515