1 """Package Install Manager for Python.
3 This is currently a MacOSX-only strawman implementation.
4 Despite other rumours the name stands for "Packman IMPlementation".
6 Tools to allow easy installation of packages. The idea is that there is
7 an online XML database per (platform, python-version) containing packages
8 known to work with that combination. This module contains tools for getting
9 and parsing the database, testing whether packages are installed, computing
10 dependencies and installing packages.
12 There is a minimal main program that works as a command line tool, but the
13 intention is that the end user will use this through a GUI.
16 from warnings
import warnpy3k
17 warnpy3k("In 3.x, the pimp module is removed.", stacklevel
=2)
27 import distutils
.sysconfig
34 __all__
= ["PimpPreferences", "PimpDatabase", "PimpPackage", "main",
35 "getDefaultDatabase", "PIMP_VERSION", "main"]
37 _scriptExc_NotInstalled
= "pimp._scriptExc_NotInstalled"
38 _scriptExc_OldInstalled
= "pimp._scriptExc_OldInstalled"
39 _scriptExc_BadInstalled
= "pimp._scriptExc_BadInstalled"
46 # source: setup-based package
47 # binary: tar (or other) archive created with setup.py bdist.
48 # installer: something that can be opened
49 DEFAULT_FLAVORORDER
=['source', 'binary', 'installer']
50 DEFAULT_DOWNLOADDIR
='/tmp'
51 DEFAULT_BUILDDIR
='/tmp'
52 DEFAULT_INSTALLDIR
=distutils
.sysconfig
.get_python_lib()
53 DEFAULT_PIMPDATABASE_FMT
="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist"
55 def getDefaultDatabase(experimental
=False):
61 major
, minor
, micro
, state
, extra
= sys
.version_info
62 pyvers
= '%d.%d' % (major
, minor
)
63 if micro
== 0 and state
!= 'final':
64 pyvers
= pyvers
+ '%s%d' % (state
, extra
)
66 longplatform
= distutils
.util
.get_platform()
67 osname
, release
, machine
= longplatform
.split('-')
68 # For some platforms we may want to differentiate between
70 if osname
== 'darwin':
71 if sys
.prefix
.startswith('/System/Library/Frameworks/Python.framework'):
72 osname
= 'darwin_apple'
73 elif sys
.prefix
.startswith('/Library/Frameworks/Python.framework'):
74 osname
= 'darwin_macpython'
75 # Otherwise we don't know...
76 # Now we try various URLs by playing with the release string.
77 # We remove numbers off the end until we find a match.
80 url
= DEFAULT_PIMPDATABASE_FMT
% (PIMP_VERSION
, status
, pyvers
, osname
, rel
, machine
)
83 except urllib2
.HTTPError
, arg
:
88 # We're out of version numbers to try. Use the
89 # full release number, this will give a reasonable
91 url
= DEFAULT_PIMPDATABASE_FMT
% (PIMP_VERSION
, status
, pyvers
, osname
, release
, machine
)
100 def _cmd(output
, dir, *cmditems
):
101 """Internal routine to run a shell command in a given directory."""
103 cmd
= ("cd \"%s\"; " % dir) + " ".join(cmditems
)
105 output
.write("+ %s\n" % cmd
)
108 child
= subprocess
.Popen(cmd
, shell
=True, stdin
=subprocess
.PIPE
,
109 stdout
=subprocess
.PIPE
, stderr
=subprocess
.STDOUT
)
112 line
= child
.stdout
.readline()
119 class PimpDownloader
:
120 """Abstract base class - Downloader for archives"""
122 def __init__(self
, argument
,
125 self
.argument
= argument
127 self
._watcher
= watcher
129 def download(self
, url
, filename
, output
=None):
132 def update(self
, str):
134 return self
._watcher
.update(str)
137 class PimpCurlDownloader(PimpDownloader
):
139 def download(self
, url
, filename
, output
=None):
140 self
.update("Downloading %s..." % url
)
141 exitstatus
= _cmd(output
, self
._dir
,
143 "--output", filename
,
145 self
.update("Downloading %s: finished" % url
)
146 return (not exitstatus
)
148 class PimpUrllibDownloader(PimpDownloader
):
150 def download(self
, url
, filename
, output
=None):
151 output
= open(filename
, 'wb')
152 self
.update("Downloading %s: opening connection" % url
)
154 download
= urllib2
.urlopen(url
)
155 if download
.headers
.has_key("content-length"):
156 length
= long(download
.headers
['content-length'])
160 data
= download
.read(4096) #read 4K at a time
164 dlsize
= dlsize
+ len(data
)
166 #this is our exit condition
169 if int(time
.time()) != lasttime
:
170 # Update at most once per second
171 lasttime
= int(time
.time())
173 keepgoing
= self
.update("Downloading %s: %d bytes..." % (url
, dlsize
))
175 keepgoing
= self
.update("Downloading %s: %d%% (%d bytes)..." % (url
, int(100.0*dlsize
/length
), dlsize
))
176 data
= download
.read(4096)
178 self
.update("Downloading %s: finished" % url
)
182 """Abstract base class - Unpacker for archives"""
186 def __init__(self
, argument
,
190 self
.argument
= argument
191 if renames
and not self
._can
_rename
:
192 raise RuntimeError, "This unpacker cannot rename files"
194 self
._renames
= renames
195 self
._watcher
= watcher
197 def unpack(self
, archive
, output
=None, package
=None):
200 def update(self
, str):
202 return self
._watcher
.update(str)
205 class PimpCommandUnpacker(PimpUnpacker
):
206 """Unpack archives by calling a Unix utility"""
210 def unpack(self
, archive
, output
=None, package
=None):
211 cmd
= self
.argument
% archive
212 if _cmd(output
, self
._dir
, cmd
):
213 return "unpack command failed"
215 class PimpTarUnpacker(PimpUnpacker
):
216 """Unpack tarfiles using the builtin tarfile module"""
220 def unpack(self
, archive
, output
=None, package
=None):
221 tf
= tarfile
.open(archive
, "r")
222 members
= tf
.getmembers()
225 for member
in members
:
226 for oldprefix
, newprefix
in self
._renames
:
227 if oldprefix
[:len(self
._dir
)] == self
._dir
:
228 oldprefix2
= oldprefix
[len(self
._dir
):]
231 if member
.name
[:len(oldprefix
)] == oldprefix
:
232 if newprefix
is None:
234 #print 'SKIP', member.name
236 member
.name
= newprefix
+ member
.name
[len(oldprefix
):]
237 print ' ', member
.name
239 elif oldprefix2
and member
.name
[:len(oldprefix2
)] == oldprefix2
:
240 if newprefix
is None:
242 #print 'SKIP', member.name
244 member
.name
= newprefix
+ member
.name
[len(oldprefix2
):]
245 #print ' ', member.name
249 #print '????', member.name
250 for member
in members
:
252 self
.update("Skipping %s" % member
.name
)
254 self
.update("Extracting %s" % member
.name
)
255 tf
.extract(member
, self
._dir
)
257 names
= [member
.name
for member
in skip
if member
.name
[-1] != '/']
259 names
= package
.filterExpectedSkips(names
)
261 return "Not all files were unpacked: %s" % " ".join(names
)
264 (".tar.Z", PimpTarUnpacker
, None),
265 (".taz", PimpTarUnpacker
, None),
266 (".tar.gz", PimpTarUnpacker
, None),
267 (".tgz", PimpTarUnpacker
, None),
268 (".tar.bz", PimpTarUnpacker
, None),
269 (".zip", PimpCommandUnpacker
, "unzip \"%s\""),
272 class PimpPreferences
:
273 """Container for per-user preferences, such as the database to use
274 and where to install packages."""
283 flavorOrder
= DEFAULT_FLAVORORDER
285 downloadDir
= DEFAULT_DOWNLOADDIR
287 buildDir
= DEFAULT_BUILDDIR
289 pimpDatabase
= getDefaultDatabase()
290 self
.setInstallDir(installDir
)
291 self
.flavorOrder
= flavorOrder
292 self
.downloadDir
= downloadDir
293 self
.buildDir
= buildDir
294 self
.pimpDatabase
= pimpDatabase
297 def setWatcher(self
, watcher
):
298 self
.watcher
= watcher
300 def setInstallDir(self
, installDir
=None):
302 # Installing to non-standard location.
303 self
.installLocations
= [
304 ('--install-lib', installDir
),
305 ('--install-headers', None),
306 ('--install-scripts', None),
307 ('--install-data', None)]
309 installDir
= DEFAULT_INSTALLDIR
310 self
.installLocations
= []
311 self
.installDir
= installDir
313 def isUserInstall(self
):
314 return self
.installDir
!= DEFAULT_INSTALLDIR
317 """Check that the preferences make sense: directories exist and are
318 writable, the install directory is on sys.path, etc."""
321 RWX_OK
= os
.R_OK|os
.W_OK|os
.X_OK
322 if not os
.path
.exists(self
.downloadDir
):
323 rv
+= "Warning: Download directory \"%s\" does not exist\n" % self
.downloadDir
324 elif not os
.access(self
.downloadDir
, RWX_OK
):
325 rv
+= "Warning: Download directory \"%s\" is not writable or not readable\n" % self
.downloadDir
326 if not os
.path
.exists(self
.buildDir
):
327 rv
+= "Warning: Build directory \"%s\" does not exist\n" % self
.buildDir
328 elif not os
.access(self
.buildDir
, RWX_OK
):
329 rv
+= "Warning: Build directory \"%s\" is not writable or not readable\n" % self
.buildDir
330 if not os
.path
.exists(self
.installDir
):
331 rv
+= "Warning: Install directory \"%s\" does not exist\n" % self
.installDir
332 elif not os
.access(self
.installDir
, RWX_OK
):
333 rv
+= "Warning: Install directory \"%s\" is not writable or not readable\n" % self
.installDir
335 installDir
= os
.path
.realpath(self
.installDir
)
338 realpath
= os
.path
.realpath(p
)
341 if installDir
== realpath
:
344 rv
+= "Warning: Install directory \"%s\" is not on sys.path\n" % self
.installDir
347 def compareFlavors(self
, left
, right
):
348 """Compare two flavor strings. This is part of your preferences
349 because whether the user prefers installing from source or binary is."""
350 if left
in self
.flavorOrder
:
351 if right
in self
.flavorOrder
:
352 return cmp(self
.flavorOrder
.index(left
), self
.flavorOrder
.index(right
))
354 if right
in self
.flavorOrder
:
356 return cmp(left
, right
)
359 """Class representing a pimp database. It can actually contain
360 information from multiple databases through inclusion, but the
361 toplevel database is considered the master, as its maintainer is
362 "responsible" for the contents."""
364 def __init__(self
, prefs
):
366 self
.preferences
= prefs
370 self
._maintainer
= ""
371 self
._description
= ""
374 def url(self
): return self
._url
375 def version(self
): return self
._version
376 def maintainer(self
): return self
._maintainer
377 def description(self
): return self
._description
382 self
.preferences
= None
384 def appendURL(self
, url
, included
=0):
385 """Append packages from the database with the given URL.
386 Only the first database should specify included=0, so the
387 global information (maintainer, description) get stored."""
389 if url
in self
._urllist
:
391 self
._urllist
.append(url
)
392 fp
= urllib2
.urlopen(url
).fp
393 plistdata
= plistlib
.Plist
.fromFile(fp
)
394 # Test here for Pimp version, etc
396 version
= plistdata
.get('Version')
397 if version
and version
> self
._version
:
398 sys
.stderr
.write("Warning: included database %s is for pimp version %s\n" %
401 self
._version
= plistdata
.get('Version')
402 if not self
._version
:
403 sys
.stderr
.write("Warning: database has no Version information\n")
404 elif self
._version
> PIMP_VERSION
:
405 sys
.stderr
.write("Warning: database version %s newer than pimp version %s\n"
406 % (self
._version
, PIMP_VERSION
))
407 self
._maintainer
= plistdata
.get('Maintainer', '')
408 self
._description
= plistdata
.get('Description', '').strip()
410 self
._appendPackages
(plistdata
['Packages'], url
)
411 others
= plistdata
.get('Include', [])
413 o
= urllib
.basejoin(url
, o
)
414 self
.appendURL(o
, included
=1)
416 def _appendPackages(self
, packages
, url
):
417 """Given a list of dictionaries containing package
418 descriptions create the PimpPackage objects and append them
419 to our internal storage."""
423 if p
.has_key('Download-URL'):
424 p
['Download-URL'] = urllib
.basejoin(url
, p
['Download-URL'])
425 flavor
= p
.get('Flavor')
426 if flavor
== 'source':
427 pkg
= PimpPackage_source(self
, p
)
428 elif flavor
== 'binary':
429 pkg
= PimpPackage_binary(self
, p
)
430 elif flavor
== 'installer':
431 pkg
= PimpPackage_installer(self
, p
)
432 elif flavor
== 'hidden':
433 pkg
= PimpPackage_installer(self
, p
)
435 pkg
= PimpPackage(self
, dict(p
))
436 self
._packages
.append(pkg
)
439 """Return a list of all PimpPackage objects in the database."""
441 return self
._packages
444 """Return a list of names of all packages in the database."""
447 for pkg
in self
._packages
:
448 rv
.append(pkg
.fullname())
452 def dump(self
, pathOrFile
):
453 """Dump the contents of the database to an XML .plist file.
455 The file can be passed as either a file object or a pathname.
456 All data, including included databases, is dumped."""
459 for pkg
in self
._packages
:
460 packages
.append(pkg
.dump())
462 'Version': self
._version
,
463 'Maintainer': self
._maintainer
,
464 'Description': self
._description
,
467 plist
= plistlib
.Plist(**plistdata
)
468 plist
.write(pathOrFile
)
470 def find(self
, ident
):
471 """Find a package. The package can be specified by name
472 or as a dictionary with name, version and flavor entries.
474 Only name is obligatory. If there are multiple matches the
475 best one (higher version number, flavors ordered according to
476 users' preference) is returned."""
478 if type(ident
) == str:
479 # Remove ( and ) for pseudo-packages
480 if ident
[0] == '(' and ident
[-1] == ')':
482 # Split into name-version-flavor
483 fields
= ident
.split('-')
484 if len(fields
) < 1 or len(fields
) > 3:
497 version
= ident
.get('Version')
498 flavor
= ident
.get('Flavor')
500 for p
in self
._packages
:
501 if name
== p
.name() and \
502 (not version
or version
== p
.version()) and \
503 (not flavor
or flavor
== p
.flavor()):
504 if not found
or found
< p
:
517 "Pre-install-command",
518 "Post-install-command",
521 "User-install-skips",
526 """Class representing a single package."""
528 def __init__(self
, db
, plistdata
):
530 name
= plistdata
["Name"]
531 for k
in plistdata
.keys():
532 if not k
in ALLOWED_KEYS
:
533 sys
.stderr
.write("Warning: %s: unknown key %s\n" % (name
, k
))
534 self
._dict
= plistdata
536 def __getitem__(self
, key
):
537 return self
._dict
[key
]
539 def name(self
): return self
._dict
['Name']
540 def version(self
): return self
._dict
.get('Version')
541 def flavor(self
): return self
._dict
.get('Flavor')
542 def description(self
): return self
._dict
['Description'].strip()
543 def shortdescription(self
): return self
.description().splitlines()[0]
544 def homepage(self
): return self
._dict
.get('Home-page')
545 def downloadURL(self
): return self
._dict
.get('Download-URL')
546 def systemwideOnly(self
): return self
._dict
.get('Systemwide-only')
549 """Return the full name "name-version-flavor" of a package.
551 If the package is a pseudo-package, something that cannot be
552 installed through pimp, return the name in (parentheses)."""
554 rv
= self
._dict
['Name']
555 if self
._dict
.has_key('Version'):
556 rv
= rv
+ '-%s' % self
._dict
['Version']
557 if self
._dict
.has_key('Flavor'):
558 rv
= rv
+ '-%s' % self
._dict
['Flavor']
559 if self
._dict
.get('Flavor') == 'hidden':
560 # Pseudo-package, show in parentheses
565 """Return a dict object containing the information on the package."""
568 def __cmp__(self
, other
):
569 """Compare two packages, where the "better" package sorts lower."""
571 if not isinstance(other
, PimpPackage
):
572 return cmp(id(self
), id(other
))
573 if self
.name() != other
.name():
574 return cmp(self
.name(), other
.name())
575 if self
.version() != other
.version():
576 return -cmp(self
.version(), other
.version())
577 return self
._db
.preferences
.compareFlavors(self
.flavor(), other
.flavor())
580 """Test wheter the package is installed.
582 Returns two values: a status indicator which is one of
583 "yes", "no", "old" (an older version is installed) or "bad"
584 (something went wrong during the install test) and a human
585 readable string which may contain more details."""
588 "NotInstalled": _scriptExc_NotInstalled
,
589 "OldInstalled": _scriptExc_OldInstalled
,
590 "BadInstalled": _scriptExc_BadInstalled
,
594 installTest
= self
._dict
['Install-test'].strip() + '\n'
596 exec installTest
in namespace
597 except ImportError, arg
:
598 return "no", str(arg
)
599 except _scriptExc_NotInstalled
, arg
:
600 return "no", str(arg
)
601 except _scriptExc_OldInstalled
, arg
:
602 return "old", str(arg
)
603 except _scriptExc_BadInstalled
, arg
:
604 return "bad", str(arg
)
606 sys
.stderr
.write("-------------------------------------\n")
607 sys
.stderr
.write("---- %s: install test got exception\n" % self
.fullname())
608 sys
.stderr
.write("---- source:\n")
609 sys
.stderr
.write(installTest
)
610 sys
.stderr
.write("---- exception:\n")
612 traceback
.print_exc(file=sys
.stderr
)
613 if self
._db
._maintainer
:
614 sys
.stderr
.write("---- Please copy this and mail to %s\n" % self
._db
._maintainer
)
615 sys
.stderr
.write("-------------------------------------\n")
616 return "bad", "Package install test got exception"
619 def prerequisites(self
):
620 """Return a list of prerequisites for this package.
622 The list contains 2-tuples, of which the first item is either
623 a PimpPackage object or None, and the second is a descriptive
624 string. The first item can be None if this package depends on
625 something that isn't pimp-installable, in which case the descriptive
626 string should tell the user what to do."""
629 if not self
._dict
.get('Download-URL'):
630 # For pseudo-packages that are already installed we don't
631 # return an error message
632 status
, _
= self
.installed()
636 "Package %s cannot be installed automatically, see the description" %
638 if self
.systemwideOnly() and self
._db
.preferences
.isUserInstall():
640 "Package %s can only be installed system-wide" %
642 if not self
._dict
.get('Prerequisites'):
644 for item
in self
._dict
['Prerequisites']:
645 if type(item
) == str:
650 if item
.has_key('Version'):
651 name
= name
+ '-' + item
['Version']
652 if item
.has_key('Flavor'):
653 name
= name
+ '-' + item
['Flavor']
654 pkg
= self
._db
.find(name
)
656 descr
= "Requires unknown %s"%name
658 descr
= pkg
.shortdescription()
659 rv
.append((pkg
, descr
))
663 def downloadPackageOnly(self
, output
=None):
664 """Download a single package, if needed.
666 An MD5 signature is used to determine whether download is needed,
667 and to test that we actually downloaded what we expected.
668 If output is given it is a file-like object that will receive a log
671 If anything unforeseen happened the method returns an error message
675 scheme
, loc
, path
, query
, frag
= urlparse
.urlsplit(self
._dict
['Download-URL'])
676 path
= urllib
.url2pathname(path
)
677 filename
= os
.path
.split(path
)[1]
678 self
.archiveFilename
= os
.path
.join(self
._db
.preferences
.downloadDir
, filename
)
679 if not self
._archiveOK
():
680 if scheme
== 'manual':
681 return "Please download package manually and save as %s" % self
.archiveFilename
682 downloader
= PimpUrllibDownloader(None, self
._db
.preferences
.downloadDir
,
683 watcher
=self
._db
.preferences
.watcher
)
684 if not downloader
.download(self
._dict
['Download-URL'],
685 self
.archiveFilename
, output
):
686 return "download command failed"
687 if not os
.path
.exists(self
.archiveFilename
) and not NO_EXECUTE
:
688 return "archive not found after download"
689 if not self
._archiveOK
():
690 return "archive does not have correct MD5 checksum"
692 def _archiveOK(self
):
693 """Test an archive. It should exist and the MD5 checksum should be correct."""
695 if not os
.path
.exists(self
.archiveFilename
):
697 if not self
._dict
.get('MD5Sum'):
698 sys
.stderr
.write("Warning: no MD5Sum for %s\n" % self
.fullname())
700 data
= open(self
.archiveFilename
, 'rb').read()
701 checksum
= hashlib
.md5(data
).hexdigest()
702 return checksum
== self
._dict
['MD5Sum']
704 def unpackPackageOnly(self
, output
=None):
705 """Unpack a downloaded package archive."""
707 filename
= os
.path
.split(self
.archiveFilename
)[1]
708 for ext
, unpackerClass
, arg
in ARCHIVE_FORMATS
:
709 if filename
[-len(ext
):] == ext
:
712 return "unknown extension for archive file: %s" % filename
713 self
.basename
= filename
[:-len(ext
)]
714 unpacker
= unpackerClass(arg
, dir=self
._db
.preferences
.buildDir
,
715 watcher
=self
._db
.preferences
.watcher
)
716 rv
= unpacker
.unpack(self
.archiveFilename
, output
=output
)
720 def installPackageOnly(self
, output
=None):
721 """Default install method, to be overridden by subclasses"""
722 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
723 % (self
.fullname(), self
._dict
.get(flavor
, ""))
725 def installSinglePackage(self
, output
=None):
726 """Download, unpack and install a single package.
728 If output is given it should be a file-like object and it
729 will receive a log of what happened."""
731 if not self
._dict
.get('Download-URL'):
732 return "%s: This package needs to be installed manually (no Download-URL field)" % self
.fullname()
733 msg
= self
.downloadPackageOnly(output
)
735 return "%s: download: %s" % (self
.fullname(), msg
)
737 msg
= self
.unpackPackageOnly(output
)
739 return "%s: unpack: %s" % (self
.fullname(), msg
)
741 return self
.installPackageOnly(output
)
743 def beforeInstall(self
):
744 """Bookkeeping before installation: remember what we have in site-packages"""
745 self
._old
_contents
= os
.listdir(self
._db
.preferences
.installDir
)
747 def afterInstall(self
):
748 """Bookkeeping after installation: interpret any new .pth files that have
751 new_contents
= os
.listdir(self
._db
.preferences
.installDir
)
752 for fn
in new_contents
:
753 if fn
in self
._old
_contents
:
755 if fn
[-4:] != '.pth':
757 fullname
= os
.path
.join(self
._db
.preferences
.installDir
, fn
)
759 for line
in f
.readlines():
764 if line
[:6] == 'import':
769 if not os
.path
.isabs(line
):
770 line
= os
.path
.join(self
._db
.preferences
.installDir
, line
)
771 line
= os
.path
.realpath(line
)
772 if not line
in sys
.path
:
773 sys
.path
.append(line
)
775 def filterExpectedSkips(self
, names
):
776 """Return a list that contains only unpexpected skips"""
777 if not self
._db
.preferences
.isUserInstall():
779 expected_skips
= self
._dict
.get('User-install-skips')
780 if not expected_skips
:
784 for skip
in expected_skips
:
785 if name
[:len(skip
)] == skip
:
788 newnames
.append(name
)
791 class PimpPackage_binary(PimpPackage
):
793 def unpackPackageOnly(self
, output
=None):
794 """We don't unpack binary packages until installing"""
797 def installPackageOnly(self
, output
=None):
798 """Install a single source package.
800 If output is given it should be a file-like object and it
801 will receive a log of what happened."""
803 if self
._dict
.has_key('Install-command'):
804 return "%s: Binary package cannot have Install-command" % self
.fullname()
806 if self
._dict
.has_key('Pre-install-command'):
807 if _cmd(output
, '/tmp', self
._dict
['Pre-install-command']):
808 return "pre-install %s: running \"%s\" failed" % \
809 (self
.fullname(), self
._dict
['Pre-install-command'])
813 # Install by unpacking
814 filename
= os
.path
.split(self
.archiveFilename
)[1]
815 for ext
, unpackerClass
, arg
in ARCHIVE_FORMATS
:
816 if filename
[-len(ext
):] == ext
:
819 return "%s: unknown extension for archive file: %s" % (self
.fullname(), filename
)
820 self
.basename
= filename
[:-len(ext
)]
823 for k
, newloc
in self
._db
.preferences
.installLocations
:
826 if k
== "--install-lib":
827 oldloc
= DEFAULT_INSTALLDIR
829 return "%s: Don't know installLocation %s" % (self
.fullname(), k
)
830 install_renames
.append((oldloc
, newloc
))
832 unpacker
= unpackerClass(arg
, dir="/", renames
=install_renames
)
833 rv
= unpacker
.unpack(self
.archiveFilename
, output
=output
, package
=self
)
839 if self
._dict
.has_key('Post-install-command'):
840 if _cmd(output
, '/tmp', self
._dict
['Post-install-command']):
841 return "%s: post-install: running \"%s\" failed" % \
842 (self
.fullname(), self
._dict
['Post-install-command'])
847 class PimpPackage_source(PimpPackage
):
849 def unpackPackageOnly(self
, output
=None):
850 """Unpack a source package and check that setup.py exists"""
851 PimpPackage
.unpackPackageOnly(self
, output
)
852 # Test that a setup script has been create
853 self
._buildDirname
= os
.path
.join(self
._db
.preferences
.buildDir
, self
.basename
)
854 setupname
= os
.path
.join(self
._buildDirname
, "setup.py")
855 if not os
.path
.exists(setupname
) and not NO_EXECUTE
:
856 return "no setup.py found after unpack of archive"
858 def installPackageOnly(self
, output
=None):
859 """Install a single source package.
861 If output is given it should be a file-like object and it
862 will receive a log of what happened."""
864 if self
._dict
.has_key('Pre-install-command'):
865 if _cmd(output
, self
._buildDirname
, self
._dict
['Pre-install-command']):
866 return "pre-install %s: running \"%s\" failed" % \
867 (self
.fullname(), self
._dict
['Pre-install-command'])
870 installcmd
= self
._dict
.get('Install-command')
871 if installcmd
and self
._install
_renames
:
872 return "Package has install-command and can only be installed to standard location"
873 # This is the "bit-bucket" for installations: everything we don't
874 # want. After installation we check that it is actually empty
875 unwanted_install_dir
= None
878 for k
, v
in self
._db
.preferences
.installLocations
:
880 # We don't want these files installed. Send them
882 if not unwanted_install_dir
:
883 unwanted_install_dir
= tempfile
.mkdtemp()
884 v
= unwanted_install_dir
885 extra_args
= extra_args
+ " %s \"%s\"" % (k
, v
)
886 installcmd
= '"%s" setup.py install %s' % (sys
.executable
, extra_args
)
887 if _cmd(output
, self
._buildDirname
, installcmd
):
888 return "install %s: running \"%s\" failed" % \
889 (self
.fullname(), installcmd
)
890 if unwanted_install_dir
and os
.path
.exists(unwanted_install_dir
):
891 unwanted_files
= os
.listdir(unwanted_install_dir
)
893 rv
= "Warning: some files were not installed: %s" % " ".join(unwanted_files
)
896 shutil
.rmtree(unwanted_install_dir
)
901 if self
._dict
.has_key('Post-install-command'):
902 if _cmd(output
, self
._buildDirname
, self
._dict
['Post-install-command']):
903 return "post-install %s: running \"%s\" failed" % \
904 (self
.fullname(), self
._dict
['Post-install-command'])
907 class PimpPackage_installer(PimpPackage
):
909 def unpackPackageOnly(self
, output
=None):
910 """We don't unpack dmg packages until installing"""
913 def installPackageOnly(self
, output
=None):
914 """Install a single source package.
916 If output is given it should be a file-like object and it
917 will receive a log of what happened."""
919 if self
._dict
.has_key('Post-install-command'):
920 return "%s: Installer package cannot have Post-install-command" % self
.fullname()
922 if self
._dict
.has_key('Pre-install-command'):
923 if _cmd(output
, '/tmp', self
._dict
['Pre-install-command']):
924 return "pre-install %s: running \"%s\" failed" % \
925 (self
.fullname(), self
._dict
['Pre-install-command'])
929 installcmd
= self
._dict
.get('Install-command')
931 if '%' in installcmd
:
932 installcmd
= installcmd
% self
.archiveFilename
934 installcmd
= 'open \"%s\"' % self
.archiveFilename
935 if _cmd(output
, "/tmp", installcmd
):
936 return '%s: install command failed (use verbose for details)' % self
.fullname()
937 return '%s: downloaded and opened. Install manually and restart Package Manager' % self
.archiveFilename
940 """Installer engine: computes dependencies and installs
941 packages in the right order."""
943 def __init__(self
, db
):
947 self
._curmessages
= []
949 def __contains__(self
, package
):
950 return package
in self
._todo
952 def _addPackages(self
, packages
):
953 for package
in packages
:
954 if not package
in self
._todo
:
955 self
._todo
.append(package
)
957 def _prepareInstall(self
, package
, force
=0, recursive
=1):
958 """Internal routine, recursive engine for prepareInstall.
960 Test whether the package is installed and (if not installed
961 or if force==1) prepend it to the temporary todo list and
962 call ourselves recursively on all prerequisites."""
965 status
, message
= package
.installed()
968 if package
in self
._todo
or package
in self
._curtodo
:
970 self
._curtodo
.insert(0, package
)
973 prereqs
= package
.prerequisites()
974 for pkg
, descr
in prereqs
:
976 self
._prepareInstall
(pkg
, False, recursive
)
978 self
._curmessages
.append("Problem with dependency: %s" % descr
)
980 def prepareInstall(self
, package
, force
=0, recursive
=1):
981 """Prepare installation of a package.
983 If the package is already installed and force is false nothing
984 is done. If recursive is true prerequisites are installed first.
986 Returns a list of packages (to be passed to install) and a list
987 of messages of any problems encountered.
991 self
._curmessages
= []
992 self
._prepareInstall
(package
, force
, recursive
)
993 rv
= self
._curtodo
, self
._curmessages
995 self
._curmessages
= []
998 def install(self
, packages
, output
):
999 """Install a list of packages."""
1001 self
._addPackages
(packages
)
1003 for pkg
in self
._todo
:
1004 msg
= pkg
.installSinglePackage(output
)
1011 def _run(mode
, verbose
, force
, args
, prefargs
, watcher
):
1012 """Engine for the main program"""
1014 prefs
= PimpPreferences(**prefargs
)
1016 prefs
.setWatcher(watcher
)
1019 sys
.stdout
.write(rv
)
1020 db
= PimpDatabase(prefs
)
1021 db
.appendURL(prefs
.pimpDatabase
)
1027 args
= db
.listnames()
1028 print "%-20.20s\t%s" % ("Package", "Description")
1030 for pkgname
in args
:
1031 pkg
= db
.find(pkgname
)
1033 description
= pkg
.shortdescription()
1034 pkgname
= pkg
.fullname()
1036 description
= 'Error: no such package'
1037 print "%-20.20s\t%s" % (pkgname
, description
)
1039 print "\tHome page:\t", pkg
.homepage()
1041 print "\tDownload URL:\t", pkg
.downloadURL()
1044 description
= pkg
.description()
1045 description
= '\n\t\t\t\t\t'.join(description
.splitlines())
1046 print "\tDescription:\t%s" % description
1047 elif mode
=='status':
1049 args
= db
.listnames()
1050 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
1052 for pkgname
in args
:
1053 pkg
= db
.find(pkgname
)
1055 status
, msg
= pkg
.installed()
1056 pkgname
= pkg
.fullname()
1059 msg
= 'No such package'
1060 print "%-20.20s\t%-9.9s\t%s" % (pkgname
, status
, msg
)
1061 if verbose
and status
== "no":
1062 prereq
= pkg
.prerequisites()
1063 for pkg
, msg
in prereq
:
1067 pkg
= pkg
.fullname()
1068 print "%-20.20s\tRequirement: %s %s" % ("", pkg
, msg
)
1069 elif mode
== 'install':
1071 print 'Please specify packages to install'
1073 inst
= PimpInstaller(db
)
1074 for pkgname
in args
:
1075 pkg
= db
.find(pkgname
)
1077 print '%s: No such package' % pkgname
1079 list, messages
= inst
.prepareInstall(pkg
, force
)
1080 if messages
and not force
:
1081 print "%s: Not installed:" % pkgname
1089 messages
= inst
.install(list, output
)
1091 print "%s: Not installed:" % pkgname
1096 """Minimal commandline tool to drive pimp."""
1100 print "Usage: pimp [options] -s [package ...] List installed status"
1101 print " pimp [options] -l [package ...] Show package information"
1102 print " pimp [options] -i package ... Install packages"
1103 print " pimp -d Dump database to stdout"
1104 print " pimp -V Print version number"
1107 print " -f Force installation"
1108 print " -D dir Set destination directory"
1109 print " (default: %s)" % DEFAULT_INSTALLDIR
1110 print " -u url URL for database"
1114 def update(self
, msg
):
1115 sys
.stderr
.write(msg
+ '\r')
1119 opts
, args
= getopt
.getopt(sys
.argv
[1:], "slifvdD:Vu:")
1120 except getopt
.GetoptError
:
1122 if not opts
and not args
:
1152 watcher
= _Watcher()
1154 prefargs
['installDir'] = a
1156 prefargs
['pimpDatabase'] = a
1159 if mode
== 'version':
1160 print 'Pimp version %s; module name is %s' % (PIMP_VERSION
, __name__
)
1162 _run(mode
, verbose
, force
, args
, prefargs
, watcher
)
1164 # Finally, try to update ourselves to a newer version.
1165 # If the end-user updates pimp through pimp the new version
1166 # will be called pimp_update and live in site-packages
1167 # or somewhere similar
1168 if __name__
!= 'pimp_update':
1174 if pimp_update
.PIMP_VERSION
<= PIMP_VERSION
:
1176 warnings
.warn("pimp_update is version %s, not newer than pimp version %s" %
1177 (pimp_update
.PIMP_VERSION
, PIMP_VERSION
))
1179 from pimp_update
import *
1181 if __name__
== '__main__':