blob: 5d037f78ec81fde6a3267f1f8262a578b175dc1f [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,
179 prerequisites=None):
180 self._db = db
181 self.name = name
182 self.version = version
183 self.flavor = flavor
184 self.description = description
185 self.longdesc = longdesc
186 self.downloadURL = downloadURL
187 self._installTest = installTest
188 self._prerequisites = prerequisites
189
190 def dump(self):
191 dict = {
192 'name': self.name,
193 }
194 if self.version:
195 dict['version'] = self.version
196 if self.flavor:
197 dict['flavor'] = self.flavor
198 if self.description:
199 dict['description'] = self.description
200 if self.longdesc:
201 dict['longdesc'] = self.longdesc
202 if self.downloadURL:
203 dict['downloadURL'] = self.downloadURL
204 if self._installTest:
205 dict['installTest'] = self._installTest
206 if self._prerequisites:
207 dict['prerequisites'] = self._prerequisites
208 return dict
209
210 def __cmp__(self, other):
211 if not isinstance(other, PimpPackage):
212 return cmp(id(self), id(other))
213 if self.name != other.name:
214 return cmp(self.name, other.name)
215 if self.version != other.version:
216 return cmp(self.version, other.version)
217 return self._db.preferences.compareFlavors(self.flavor, other.flavor)
218
219 def installed(self):
220 namespace = {
221 "NotInstalled": _scriptExc_NotInstalled,
222 "OldInstalled": _scriptExc_OldInstalled,
223 "BadInstalled": _scriptExc_BadInstalled,
224 "os": os,
225 "sys": sys,
226 }
227 installTest = self._installTest.strip() + '\n'
228 try:
229 exec installTest in namespace
230 except ImportError, arg:
231 return "no", str(arg)
232 except _scriptExc_NotInstalled, arg:
233 return "no", str(arg)
234 except _scriptExc_OldInstalled, arg:
235 return "old", str(arg)
236 except _scriptExc_BadInstalled, arg:
237 return "bad", str(arg)
238 except:
239 print 'TEST:', repr(self._installTest)
240 return "bad", "Package install test got exception"
241 return "yes", ""
242
243 def prerequisites(self):
244 rv = []
245 if not self.downloadURL:
246 return [(None, "This package needs to be installed manually")]
247 if not self._prerequisites:
248 return []
249 for item in self._prerequisites:
250 if type(item) == str:
251 pkg = None
252 descr = str(item)
253 else:
254 pkg = self._db.find(item)
255 if not pkg:
256 descr = "Requires unknown %s"%_fmtpackagename(item)
257 else:
258 descr = pkg.description
259 rv.append((pkg, descr))
260 return rv
261
262 def _cmd(self, output, dir, *cmditems):
263 cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
264 if output:
265 output.write("+ %s\n" % cmd)
266 if NO_EXECUTE:
267 return 0
268 fp = os.popen(cmd, "r")
269 while 1:
270 line = fp.readline()
271 if not line:
272 break
273 if output:
274 output.write(line)
275 rv = fp.close()
276 return rv
277
278 def downloadSinglePackage(self, output):
279 scheme, loc, path, query, frag = urlparse.urlsplit(self.downloadURL)
280 path = urllib.url2pathname(path)
281 filename = os.path.split(path)[1]
282 self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
283 if self._cmd(output, self._db.preferences.downloadDir, "curl",
284 "--output", self.archiveFilename,
285 self.downloadURL):
286 return "download command failed"
287 if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
288 return "archive not found after download"
289
290 def unpackSinglePackage(self, output):
291 filename = os.path.split(self.archiveFilename)[1]
292 for ext, cmd in ARCHIVE_FORMATS:
293 if filename[-len(ext):] == ext:
294 break
295 else:
296 return "unknown extension for archive file: %s" % filename
297 basename = filename[:-len(ext)]
298 cmd = cmd % self.archiveFilename
299 self._buildDirname = os.path.join(self._db.preferences.buildDir, basename)
300 if self._cmd(output, self._db.preferences.buildDir, cmd):
301 return "unpack command failed"
302 setupname = os.path.join(self._buildDirname, "setup.py")
303 if not os.path.exists(setupname) and not NO_EXECUTE:
304 return "no setup.py found after unpack of archive"
305
306 def installSinglePackage(self, output):
307 if not self.downloadURL:
308 return "%s: This package needs to be installed manually" % _fmtpackagename(self)
309 msg = self.downloadSinglePackage(output)
310 if msg:
311 return "download %s: %s" % (_fmtpackagename(self), msg)
312 msg = self.unpackSinglePackage(output)
313 if msg:
314 return "unpack %s: %s" % (_fmtpackagename(self), msg)
315 if self._cmd(output, self._buildDirname, sys.executable, "setup.py install"):
316 return "install %s: running \"setup.py install\" failed" % _fmtpackagename(self)
317 return None
318
319class PimpInstaller:
320 def __init__(self, db):
321 self._todo = []
322 self._db = db
323 self._curtodo = []
324 self._curmessages = []
325
326 def __contains__(self, package):
327 return package in self._todo
328
329 def _addPackages(self, packages):
330 for package in packages:
331 if not package in self._todo:
332 self._todo.insert(0, package)
333
334 def _prepareInstall(self, package, force=0, recursive=1):
335 if not force:
336 status, message = package.installed()
337 if status == "yes":
338 return
339 if package in self._todo or package in self._curtodo:
340 return
341 self._curtodo.insert(0, package)
342 if not recursive:
343 return
344 prereqs = package.prerequisites()
345 for pkg, descr in prereqs:
346 if pkg:
347 self._prepareInstall(pkg, force, recursive)
348 else:
349 self._curmessages.append("Requires: %s" % descr)
350
351 def prepareInstall(self, package, force=0, recursive=1):
352 self._curtodo = []
353 self._curmessages = []
354 self._prepareInstall(package, force, recursive)
355 rv = self._curtodo, self._curmessages
356 self._curtodo = []
357 self._curmessages = []
358 return rv
359
360 def install(self, packages, output):
361 self._addPackages(packages)
362 status = []
363 for pkg in self._todo:
364 msg = pkg.installSinglePackage(output)
365 if msg:
366 status.append(msg)
367 return status
368
369
370def _fmtpackagename(dict):
371 if isinstance(dict, PimpPackage):
372 dict = dict.dump()
373 rv = dict['name']
374 if dict.has_key('version'):
375 rv = rv + '-%s' % dict['version']
376 if dict.has_key('flavor'):
377 rv = rv + '-%s' % dict['flavor']
378 if not dict.get('downloadURL'):
379 # Pseudo-package, show in parentheses
380 rv = '(%s)' % rv
381 return rv
382
383def _run(mode, verbose, force, args):
384 prefs = PimpPreferences()
385 prefs.check()
386 db = PimpDatabase(prefs)
387 db.appendURL(prefs.pimpDatabase)
388
389 if mode =='list':
390 if not args:
391 args = db.listnames()
392 print "%-20.20s\t%s" % ("Package", "Description")
393 print
394 for pkgname in args:
395 pkg = db.find(pkgname)
396 if pkg:
397 description = pkg.description
398 pkgname = _fmtpackagename(pkg)
399 else:
400 description = 'Error: no such package'
401 print "%-20.20s\t%s" % (pkgname, description)
402 if verbose:
403 print "\tHome page:\t", pkg.longdesc
404 print "\tDownload URL:\t", pkg.downloadURL
405 if mode =='status':
406 if not args:
407 args = db.listnames()
408 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
409 print
410 for pkgname in args:
411 pkg = db.find(pkgname)
412 if pkg:
413 status, msg = pkg.installed()
414 pkgname = _fmtpackagename(pkg)
415 else:
416 status = 'error'
417 msg = 'No such package'
418 print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
419 if verbose and status == "no":
420 prereq = pkg.prerequisites()
421 for pkg, msg in prereq:
422 if not pkg:
423 pkg = ''
424 else:
425 pkg = _fmtpackagename(pkg)
426 print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
427 elif mode == 'install':
428 if not args:
429 print 'Please specify packages to install'
430 sys.exit(1)
431 inst = PimpInstaller(db)
432 for pkgname in args:
433 pkg = db.find(pkgname)
434 if not pkg:
435 print '%s: No such package' % pkgname
436 continue
437 list, messages = inst.prepareInstall(pkg, force)
438 if messages and not force:
439 print "%s: Not installed:" % pkgname
440 for m in messages:
441 print "\t", m
442 else:
443 if verbose:
444 output = sys.stdout
445 else:
446 output = None
447 messages = inst.install(list, output)
448 if messages:
449 print "%s: Not installed:" % pkgname
450 for m in messages:
451 print "\t", m
452
453def main():
454 import getopt
455 def _help():
456 print "Usage: pimp [-v] -s [package ...] List installed status"
457 print " pimp [-v] -l [package ...] Show package information"
458 print " pimp [-vf] -i package ... Install packages"
459 print "Options:"
460 print " -v Verbose"
461 print " -f Force installation"
462 sys.exit(1)
463
464 try:
465 opts, args = getopt.getopt(sys.argv[1:], "slifv")
466 except getopt.Error:
467 _help()
468 if not opts and not args:
469 _help()
470 mode = None
471 force = 0
472 verbose = 0
473 for o, a in opts:
474 if o == '-s':
475 if mode:
476 _help()
477 mode = 'status'
478 if o == '-l':
479 if mode:
480 _help()
481 mode = 'list'
482 if o == '-L':
483 if mode:
484 _help()
485 mode = 'longlist'
486 if o == '-i':
487 mode = 'install'
488 if o == '-f':
489 force = 1
490 if o == '-v':
491 verbose = 1
492 if not mode:
493 _help()
494 _run(mode, verbose, force, args)
495
496if __name__ == '__main__':
497 main()
498
499