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.
23 import distutils
.sysconfig
30 __all__
= ["PimpPreferences", "PimpDatabase", "PimpPackage", "main",
31 "getDefaultDatabase", "PIMP_VERSION", "main"]
33 _scriptExc_NotInstalled
= "pimp._scriptExc_NotInstalled"
34 _scriptExc_OldInstalled
= "pimp._scriptExc_OldInstalled"
35 _scriptExc_BadInstalled
= "pimp._scriptExc_BadInstalled"
42 # source: setup-based package
43 # binary: tar (or other) archive created with setup.py bdist.
44 # installer: something that can be opened
45 DEFAULT_FLAVORORDER
=['source', 'binary', 'installer']
46 DEFAULT_DOWNLOADDIR
='/tmp'
47 DEFAULT_BUILDDIR
='/tmp'
48 DEFAULT_INSTALLDIR
=distutils
.sysconfig
.get_python_lib()
49 DEFAULT_PIMPDATABASE_FMT
="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist"
51 def getDefaultDatabase(experimental
=False):
57 major
, minor
, micro
, state
, extra
= sys
.version_info
58 pyvers
= '%d.%d' % (major
, minor
)
59 if micro
== 0 and state
!= 'final':
60 pyvers
= pyvers
+ '%s%d' % (state
, extra
)
62 longplatform
= distutils
.util
.get_platform()
63 osname
, release
, machine
= longplatform
.split('-')
64 # For some platforms we may want to differentiate between
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.
76 url
= DEFAULT_PIMPDATABASE_FMT
% (PIMP_VERSION
, status
, pyvers
, osname
, rel
, machine
)
79 except urllib2
.HTTPError
, arg
:
84 # We're out of version numbers to try. Use the
85 # full release number, this will give a reasonable
87 url
= DEFAULT_PIMPDATABASE_FMT
% (PIMP_VERSION
, status
, pyvers
, osname
, release
, machine
)
96 def _cmd(output
, dir, *cmditems
):
97 """Internal routine to run a shell command in a given directory."""
99 cmd
= ("cd \"%s\"; " % dir) + " ".join(cmditems
)
101 output
.write("+ %s\n" % cmd
)
104 child
= popen2
.Popen4(cmd
)
105 child
.tochild
.close()
107 line
= child
.fromchild
.readline()
114 class PimpDownloader
:
115 """Abstract base class - Downloader for archives"""
117 def __init__(self
, argument
,
120 self
.argument
= argument
122 self
._watcher
= watcher
124 def download(self
, url
, filename
, output
=None):
127 def update(self
, str):
129 return self
._watcher
.update(str)
132 class PimpCurlDownloader(PimpDownloader
):
134 def download(self
, url
, filename
, output
=None):
135 self
.update("Downloading %s..." % url
)
136 exitstatus
= _cmd(output
, self
._dir
,
138 "--output", filename
,
140 self
.update("Downloading %s: finished" % url
)
141 return (not exitstatus
)
143 class PimpUrllibDownloader(PimpDownloader
):
145 def download(self
, url
, filename
, output
=None):
146 output
= open(filename
, 'wb')
147 self
.update("Downloading %s: opening connection" % url
)
149 download
= urllib2
.urlopen(url
)
150 if download
.headers
.has_key("content-length"):
151 length
= long(download
.headers
['content-length'])
155 data
= download
.read(4096) #read 4K at a time
159 dlsize
= dlsize
+ len(data
)
161 #this is our exit condition
164 if int(time
.time()) != lasttime
:
165 # Update at most once per second
166 lasttime
= int(time
.time())
168 keepgoing
= self
.update("Downloading %s: %d bytes..." % (url
, dlsize
))
170 keepgoing
= self
.update("Downloading %s: %d%% (%d bytes)..." % (url
, int(100.0*dlsize
/length
), dlsize
))
171 data
= download
.read(4096)
173 self
.update("Downloading %s: finished" % url
)
177 """Abstract base class - Unpacker for archives"""
181 def __init__(self
, argument
,
185 self
.argument
= argument
186 if renames
and not self
._can
_rename
:
187 raise RuntimeError, "This unpacker cannot rename files"
189 self
._renames
= renames
190 self
._watcher
= watcher
192 def unpack(self
, archive
, output
=None, package
=None):
195 def update(self
, str):
197 return self
._watcher
.update(str)
200 class PimpCommandUnpacker(PimpUnpacker
):
201 """Unpack archives by calling a Unix utility"""
205 def unpack(self
, archive
, output
=None, package
=None):
206 cmd
= self
.argument
% archive
207 if _cmd(output
, self
._dir
, cmd
):
208 return "unpack command failed"
210 class PimpTarUnpacker(PimpUnpacker
):
211 """Unpack tarfiles using the builtin tarfile module"""
215 def unpack(self
, archive
, output
=None, package
=None):
216 tf
= tarfile
.open(archive
, "r")
217 members
= tf
.getmembers()
220 for member
in members
:
221 for oldprefix
, newprefix
in self
._renames
:
222 if oldprefix
[:len(self
._dir
)] == self
._dir
:
223 oldprefix2
= oldprefix
[len(self
._dir
):]
226 if member
.name
[:len(oldprefix
)] == oldprefix
:
227 if newprefix
is None:
229 #print 'SKIP', member.name
231 member
.name
= newprefix
+ member
.name
[len(oldprefix
):]
232 print ' ', member
.name
234 elif oldprefix2
and member
.name
[:len(oldprefix2
)] == oldprefix2
:
235 if newprefix
is None:
237 #print 'SKIP', member.name
239 member
.name
= newprefix
+ member
.name
[len(oldprefix2
):]
240 #print ' ', member.name
244 #print '????', member.name
245 for member
in members
:
247 self
.update("Skipping %s" % member
.name
)
249 self
.update("Extracting %s" % member
.name
)
250 tf
.extract(member
, self
._dir
)
252 names
= [member
.name
for member
in skip
if member
.name
[-1] != '/']
254 names
= package
.filterExpectedSkips(names
)
256 return "Not all files were unpacked: %s" % " ".join(names
)
259 (".tar.Z", PimpTarUnpacker
, None),
260 (".taz", PimpTarUnpacker
, None),
261 (".tar.gz", PimpTarUnpacker
, None),
262 (".tgz", PimpTarUnpacker
, None),
263 (".tar.bz", PimpTarUnpacker
, None),
264 (".zip", PimpCommandUnpacker
, "unzip \"%s\""),
267 class PimpPreferences
:
268 """Container for per-user preferences, such as the database to use
269 and where to install packages."""
278 flavorOrder
= DEFAULT_FLAVORORDER
280 downloadDir
= DEFAULT_DOWNLOADDIR
282 buildDir
= DEFAULT_BUILDDIR
284 pimpDatabase
= getDefaultDatabase()
285 self
.setInstallDir(installDir
)
286 self
.flavorOrder
= flavorOrder
287 self
.downloadDir
= downloadDir
288 self
.buildDir
= buildDir
289 self
.pimpDatabase
= pimpDatabase
292 def setWatcher(self
, watcher
):
293 self
.watcher
= watcher
295 def setInstallDir(self
, installDir
=None):
297 # Installing to non-standard location.
298 self
.installLocations
= [
299 ('--install-lib', installDir
),
300 ('--install-headers', None),
301 ('--install-scripts', None),
302 ('--install-data', None)]
304 installDir
= DEFAULT_INSTALLDIR
305 self
.installLocations
= []
306 self
.installDir
= installDir
308 def isUserInstall(self
):
309 return self
.installDir
!= DEFAULT_INSTALLDIR
312 """Check that the preferences make sense: directories exist and are
313 writable, the install directory is on sys.path, etc."""
316 RWX_OK
= os
.R_OK|os
.W_OK|os
.X_OK
317 if not os
.path
.exists(self
.downloadDir
):
318 rv
+= "Warning: Download directory \"%s\" does not exist\n" % self
.downloadDir
319 elif not os
.access(self
.downloadDir
, RWX_OK
):
320 rv
+= "Warning: Download directory \"%s\" is not writable or not readable\n" % self
.downloadDir
321 if not os
.path
.exists(self
.buildDir
):
322 rv
+= "Warning: Build directory \"%s\" does not exist\n" % self
.buildDir
323 elif not os
.access(self
.buildDir
, RWX_OK
):
324 rv
+= "Warning: Build directory \"%s\" is not writable or not readable\n" % self
.buildDir
325 if not os
.path
.exists(self
.installDir
):
326 rv
+= "Warning: Install directory \"%s\" does not exist\n" % self
.installDir
327 elif not os
.access(self
.installDir
, RWX_OK
):
328 rv
+= "Warning: Install directory \"%s\" is not writable or not readable\n" % self
.installDir
330 installDir
= os
.path
.realpath(self
.installDir
)
333 realpath
= os
.path
.realpath(p
)
336 if installDir
== realpath
:
339 rv
+= "Warning: Install directory \"%s\" is not on sys.path\n" % self
.installDir
342 def compareFlavors(self
, left
, right
):
343 """Compare two flavor strings. This is part of your preferences
344 because whether the user prefers installing from source or binary is."""
345 if left
in self
.flavorOrder
:
346 if right
in self
.flavorOrder
:
347 return cmp(self
.flavorOrder
.index(left
), self
.flavorOrder
.index(right
))
349 if right
in self
.flavorOrder
:
351 return cmp(left
, right
)
354 """Class representing a pimp database. It can actually contain
355 information from multiple databases through inclusion, but the
356 toplevel database is considered the master, as its maintainer is
357 "responsible" for the contents."""
359 def __init__(self
, prefs
):
361 self
.preferences
= prefs
365 self
._maintainer
= ""
366 self
._description
= ""
369 def url(self
): return self
._url
370 def version(self
): return self
._version
371 def maintainer(self
): return self
._maintainer
372 def description(self
): return self
._description
377 self
.preferences
= None
379 def appendURL(self
, url
, included
=0):
380 """Append packages from the database with the given URL.
381 Only the first database should specify included=0, so the
382 global information (maintainer, description) get stored."""
384 if url
in self
._urllist
:
386 self
._urllist
.append(url
)
387 fp
= urllib2
.urlopen(url
).fp
388 plistdata
= plistlib
.Plist
.fromFile(fp
)
389 # Test here for Pimp version, etc
391 version
= plistdata
.get('Version')
392 if version
and version
> self
._version
:
393 sys
.stderr
.write("Warning: included database %s is for pimp version %s\n" %
396 self
._version
= plistdata
.get('Version')
397 if not self
._version
:
398 sys
.stderr
.write("Warning: database has no Version information\n")
399 elif self
._version
> PIMP_VERSION
:
400 sys
.stderr
.write("Warning: database version %s newer than pimp version %s\n"
401 % (self
._version
, PIMP_VERSION
))
402 self
._maintainer
= plistdata
.get('Maintainer', '')
403 self
._description
= plistdata
.get('Description', '').strip()
405 self
._appendPackages
(plistdata
['Packages'], url
)
406 others
= plistdata
.get('Include', [])
408 o
= urllib
.basejoin(url
, o
)
409 self
.appendURL(o
, included
=1)
411 def _appendPackages(self
, packages
, url
):
412 """Given a list of dictionaries containing package
413 descriptions create the PimpPackage objects and append them
414 to our internal storage."""
418 if p
.has_key('Download-URL'):
419 p
['Download-URL'] = urllib
.basejoin(url
, p
['Download-URL'])
420 flavor
= p
.get('Flavor')
421 if flavor
== 'source':
422 pkg
= PimpPackage_source(self
, p
)
423 elif flavor
== 'binary':
424 pkg
= PimpPackage_binary(self
, p
)
425 elif flavor
== 'installer':
426 pkg
= PimpPackage_installer(self
, p
)
427 elif flavor
== 'hidden':
428 pkg
= PimpPackage_installer(self
, p
)
430 pkg
= PimpPackage(self
, dict(p
))
431 self
._packages
.append(pkg
)
434 """Return a list of all PimpPackage objects in the database."""
436 return self
._packages
439 """Return a list of names of all packages in the database."""
442 for pkg
in self
._packages
:
443 rv
.append(pkg
.fullname())
447 def dump(self
, pathOrFile
):
448 """Dump the contents of the database to an XML .plist file.
450 The file can be passed as either a file object or a pathname.
451 All data, including included databases, is dumped."""
454 for pkg
in self
._packages
:
455 packages
.append(pkg
.dump())
457 'Version': self
._version
,
458 'Maintainer': self
._maintainer
,
459 'Description': self
._description
,
462 plist
= plistlib
.Plist(**plistdata
)
463 plist
.write(pathOrFile
)
465 def find(self
, ident
):
466 """Find a package. The package can be specified by name
467 or as a dictionary with name, version and flavor entries.
469 Only name is obligatory. If there are multiple matches the
470 best one (higher version number, flavors ordered according to
471 users' preference) is returned."""
473 if type(ident
) == str:
474 # Remove ( and ) for pseudo-packages
475 if ident
[0] == '(' and ident
[-1] == ')':
477 # Split into name-version-flavor
478 fields
= ident
.split('-')
479 if len(fields
) < 1 or len(fields
) > 3:
492 version
= ident
.get('Version')
493 flavor
= ident
.get('Flavor')
495 for p
in self
._packages
:
496 if name
== p
.name() and \
497 (not version
or version
== p
.version()) and \
498 (not flavor
or flavor
== p
.flavor()):
499 if not found
or found
< p
:
512 "Pre-install-command",
513 "Post-install-command",
516 "User-install-skips",
521 """Class representing a single package."""
523 def __init__(self
, db
, plistdata
):
525 name
= plistdata
["Name"]
526 for k
in plistdata
.keys():
527 if not k
in ALLOWED_KEYS
:
528 sys
.stderr
.write("Warning: %s: unknown key %s\n" % (name
, k
))
529 self
._dict
= plistdata
531 def __getitem__(self
, key
):
532 return self
._dict
[key
]
534 def name(self
): return self
._dict
['Name']
535 def version(self
): return self
._dict
.get('Version')
536 def flavor(self
): return self
._dict
.get('Flavor')
537 def description(self
): return self
._dict
['Description'].strip()
538 def shortdescription(self
): return self
.description().splitlines()[0]
539 def homepage(self
): return self
._dict
.get('Home-page')
540 def downloadURL(self
): return self
._dict
.get('Download-URL')
541 def systemwideOnly(self
): return self
._dict
.get('Systemwide-only')
544 """Return the full name "name-version-flavor" of a package.
546 If the package is a pseudo-package, something that cannot be
547 installed through pimp, return the name in (parentheses)."""
549 rv
= self
._dict
['Name']
550 if self
._dict
.has_key('Version'):
551 rv
= rv
+ '-%s' % self
._dict
['Version']
552 if self
._dict
.has_key('Flavor'):
553 rv
= rv
+ '-%s' % self
._dict
['Flavor']
554 if self
._dict
.get('Flavor') == 'hidden':
555 # Pseudo-package, show in parentheses
560 """Return a dict object containing the information on the package."""
563 def __cmp__(self
, other
):
564 """Compare two packages, where the "better" package sorts lower."""
566 if not isinstance(other
, PimpPackage
):
567 return cmp(id(self
), id(other
))
568 if self
.name() != other
.name():
569 return cmp(self
.name(), other
.name())
570 if self
.version() != other
.version():
571 return -cmp(self
.version(), other
.version())
572 return self
._db
.preferences
.compareFlavors(self
.flavor(), other
.flavor())
575 """Test wheter the package is installed.
577 Returns two values: a status indicator which is one of
578 "yes", "no", "old" (an older version is installed) or "bad"
579 (something went wrong during the install test) and a human
580 readable string which may contain more details."""
583 "NotInstalled": _scriptExc_NotInstalled
,
584 "OldInstalled": _scriptExc_OldInstalled
,
585 "BadInstalled": _scriptExc_BadInstalled
,
589 installTest
= self
._dict
['Install-test'].strip() + '\n'
591 exec installTest
in namespace
592 except ImportError, arg
:
593 return "no", str(arg
)
594 except _scriptExc_NotInstalled
, arg
:
595 return "no", str(arg
)
596 except _scriptExc_OldInstalled
, arg
:
597 return "old", str(arg
)
598 except _scriptExc_BadInstalled
, arg
:
599 return "bad", str(arg
)
601 sys
.stderr
.write("-------------------------------------\n")
602 sys
.stderr
.write("---- %s: install test got exception\n" % self
.fullname())
603 sys
.stderr
.write("---- source:\n")
604 sys
.stderr
.write(installTest
)
605 sys
.stderr
.write("---- exception:\n")
607 traceback
.print_exc(file=sys
.stderr
)
608 if self
._db
._maintainer
:
609 sys
.stderr
.write("---- Please copy this and mail to %s\n" % self
._db
._maintainer
)
610 sys
.stderr
.write("-------------------------------------\n")
611 return "bad", "Package install test got exception"
614 def prerequisites(self
):
615 """Return a list of prerequisites for this package.
617 The list contains 2-tuples, of which the first item is either
618 a PimpPackage object or None, and the second is a descriptive
619 string. The first item can be None if this package depends on
620 something that isn't pimp-installable, in which case the descriptive
621 string should tell the user what to do."""
624 if not self
._dict
.get('Download-URL'):
625 # For pseudo-packages that are already installed we don't
626 # return an error message
627 status
, _
= self
.installed()
631 "Package %s cannot be installed automatically, see the description" %
633 if self
.systemwideOnly() and self
._db
.preferences
.isUserInstall():
635 "Package %s can only be installed system-wide" %
637 if not self
._dict
.get('Prerequisites'):
639 for item
in self
._dict
['Prerequisites']:
640 if type(item
) == str:
645 if item
.has_key('Version'):
646 name
= name
+ '-' + item
['Version']
647 if item
.has_key('Flavor'):
648 name
= name
+ '-' + item
['Flavor']
649 pkg
= self
._db
.find(name
)
651 descr
= "Requires unknown %s"%name
653 descr
= pkg
.shortdescription()
654 rv
.append((pkg
, descr
))
658 def downloadPackageOnly(self
, output
=None):
659 """Download a single package, if needed.
661 An MD5 signature is used to determine whether download is needed,
662 and to test that we actually downloaded what we expected.
663 If output is given it is a file-like object that will receive a log
666 If anything unforeseen happened the method returns an error message
670 scheme
, loc
, path
, query
, frag
= urlparse
.urlsplit(self
._dict
['Download-URL'])
671 path
= urllib
.url2pathname(path
)
672 filename
= os
.path
.split(path
)[1]
673 self
.archiveFilename
= os
.path
.join(self
._db
.preferences
.downloadDir
, filename
)
674 if not self
._archiveOK
():
675 if scheme
== 'manual':
676 return "Please download package manually and save as %s" % self
.archiveFilename
677 downloader
= PimpUrllibDownloader(None, self
._db
.preferences
.downloadDir
,
678 watcher
=self
._db
.preferences
.watcher
)
679 if not downloader
.download(self
._dict
['Download-URL'],
680 self
.archiveFilename
, output
):
681 return "download command failed"
682 if not os
.path
.exists(self
.archiveFilename
) and not NO_EXECUTE
:
683 return "archive not found after download"
684 if not self
._archiveOK
():
685 return "archive does not have correct MD5 checksum"
687 def _archiveOK(self
):
688 """Test an archive. It should exist and the MD5 checksum should be correct."""
690 if not os
.path
.exists(self
.archiveFilename
):
692 if not self
._dict
.get('MD5Sum'):
693 sys
.stderr
.write("Warning: no MD5Sum for %s\n" % self
.fullname())
695 data
= open(self
.archiveFilename
, 'rb').read()
696 checksum
= hashlib
.md5(data
).hexdigest()
697 return checksum
== self
._dict
['MD5Sum']
699 def unpackPackageOnly(self
, output
=None):
700 """Unpack a downloaded package archive."""
702 filename
= os
.path
.split(self
.archiveFilename
)[1]
703 for ext
, unpackerClass
, arg
in ARCHIVE_FORMATS
:
704 if filename
[-len(ext
):] == ext
:
707 return "unknown extension for archive file: %s" % filename
708 self
.basename
= filename
[:-len(ext
)]
709 unpacker
= unpackerClass(arg
, dir=self
._db
.preferences
.buildDir
,
710 watcher
=self
._db
.preferences
.watcher
)
711 rv
= unpacker
.unpack(self
.archiveFilename
, output
=output
)
715 def installPackageOnly(self
, output
=None):
716 """Default install method, to be overridden by subclasses"""
717 return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
718 % (self
.fullname(), self
._dict
.get(flavor
, ""))
720 def installSinglePackage(self
, output
=None):
721 """Download, unpack and install a single package.
723 If output is given it should be a file-like object and it
724 will receive a log of what happened."""
726 if not self
._dict
.get('Download-URL'):
727 return "%s: This package needs to be installed manually (no Download-URL field)" % self
.fullname()
728 msg
= self
.downloadPackageOnly(output
)
730 return "%s: download: %s" % (self
.fullname(), msg
)
732 msg
= self
.unpackPackageOnly(output
)
734 return "%s: unpack: %s" % (self
.fullname(), msg
)
736 return self
.installPackageOnly(output
)
738 def beforeInstall(self
):
739 """Bookkeeping before installation: remember what we have in site-packages"""
740 self
._old
_contents
= os
.listdir(self
._db
.preferences
.installDir
)
742 def afterInstall(self
):
743 """Bookkeeping after installation: interpret any new .pth files that have
746 new_contents
= os
.listdir(self
._db
.preferences
.installDir
)
747 for fn
in new_contents
:
748 if fn
in self
._old
_contents
:
750 if fn
[-4:] != '.pth':
752 fullname
= os
.path
.join(self
._db
.preferences
.installDir
, fn
)
754 for line
in f
.readlines():
759 if line
[:6] == 'import':
764 if not os
.path
.isabs(line
):
765 line
= os
.path
.join(self
._db
.preferences
.installDir
, line
)
766 line
= os
.path
.realpath(line
)
767 if not line
in sys
.path
:
768 sys
.path
.append(line
)
770 def filterExpectedSkips(self
, names
):
771 """Return a list that contains only unpexpected skips"""
772 if not self
._db
.preferences
.isUserInstall():
774 expected_skips
= self
._dict
.get('User-install-skips')
775 if not expected_skips
:
779 for skip
in expected_skips
:
780 if name
[:len(skip
)] == skip
:
783 newnames
.append(name
)
786 class PimpPackage_binary(PimpPackage
):
788 def unpackPackageOnly(self
, output
=None):
789 """We don't unpack binary packages until installing"""
792 def installPackageOnly(self
, output
=None):
793 """Install a single source package.
795 If output is given it should be a file-like object and it
796 will receive a log of what happened."""
798 if self
._dict
.has_key('Install-command'):
799 return "%s: Binary package cannot have Install-command" % self
.fullname()
801 if self
._dict
.has_key('Pre-install-command'):
802 if _cmd(output
, '/tmp', self
._dict
['Pre-install-command']):
803 return "pre-install %s: running \"%s\" failed" % \
804 (self
.fullname(), self
._dict
['Pre-install-command'])
808 # Install by unpacking
809 filename
= os
.path
.split(self
.archiveFilename
)[1]
810 for ext
, unpackerClass
, arg
in ARCHIVE_FORMATS
:
811 if filename
[-len(ext
):] == ext
:
814 return "%s: unknown extension for archive file: %s" % (self
.fullname(), filename
)
815 self
.basename
= filename
[:-len(ext
)]
818 for k
, newloc
in self
._db
.preferences
.installLocations
:
821 if k
== "--install-lib":
822 oldloc
= DEFAULT_INSTALLDIR
824 return "%s: Don't know installLocation %s" % (self
.fullname(), k
)
825 install_renames
.append((oldloc
, newloc
))
827 unpacker
= unpackerClass(arg
, dir="/", renames
=install_renames
)
828 rv
= unpacker
.unpack(self
.archiveFilename
, output
=output
, package
=self
)
834 if self
._dict
.has_key('Post-install-command'):
835 if _cmd(output
, '/tmp', self
._dict
['Post-install-command']):
836 return "%s: post-install: running \"%s\" failed" % \
837 (self
.fullname(), self
._dict
['Post-install-command'])
842 class PimpPackage_source(PimpPackage
):
844 def unpackPackageOnly(self
, output
=None):
845 """Unpack a source package and check that setup.py exists"""
846 PimpPackage
.unpackPackageOnly(self
, output
)
847 # Test that a setup script has been create
848 self
._buildDirname
= os
.path
.join(self
._db
.preferences
.buildDir
, self
.basename
)
849 setupname
= os
.path
.join(self
._buildDirname
, "setup.py")
850 if not os
.path
.exists(setupname
) and not NO_EXECUTE
:
851 return "no setup.py found after unpack of archive"
853 def installPackageOnly(self
, output
=None):
854 """Install a single source package.
856 If output is given it should be a file-like object and it
857 will receive a log of what happened."""
859 if self
._dict
.has_key('Pre-install-command'):
860 if _cmd(output
, self
._buildDirname
, self
._dict
['Pre-install-command']):
861 return "pre-install %s: running \"%s\" failed" % \
862 (self
.fullname(), self
._dict
['Pre-install-command'])
865 installcmd
= self
._dict
.get('Install-command')
866 if installcmd
and self
._install
_renames
:
867 return "Package has install-command and can only be installed to standard location"
868 # This is the "bit-bucket" for installations: everything we don't
869 # want. After installation we check that it is actually empty
870 unwanted_install_dir
= None
873 for k
, v
in self
._db
.preferences
.installLocations
:
875 # We don't want these files installed. Send them
877 if not unwanted_install_dir
:
878 unwanted_install_dir
= tempfile
.mkdtemp()
879 v
= unwanted_install_dir
880 extra_args
= extra_args
+ " %s \"%s\"" % (k
, v
)
881 installcmd
= '"%s" setup.py install %s' % (sys
.executable
, extra_args
)
882 if _cmd(output
, self
._buildDirname
, installcmd
):
883 return "install %s: running \"%s\" failed" % \
884 (self
.fullname(), installcmd
)
885 if unwanted_install_dir
and os
.path
.exists(unwanted_install_dir
):
886 unwanted_files
= os
.listdir(unwanted_install_dir
)
888 rv
= "Warning: some files were not installed: %s" % " ".join(unwanted_files
)
891 shutil
.rmtree(unwanted_install_dir
)
896 if self
._dict
.has_key('Post-install-command'):
897 if _cmd(output
, self
._buildDirname
, self
._dict
['Post-install-command']):
898 return "post-install %s: running \"%s\" failed" % \
899 (self
.fullname(), self
._dict
['Post-install-command'])
902 class PimpPackage_installer(PimpPackage
):
904 def unpackPackageOnly(self
, output
=None):
905 """We don't unpack dmg packages until installing"""
908 def installPackageOnly(self
, output
=None):
909 """Install a single source package.
911 If output is given it should be a file-like object and it
912 will receive a log of what happened."""
914 if self
._dict
.has_key('Post-install-command'):
915 return "%s: Installer package cannot have Post-install-command" % self
.fullname()
917 if self
._dict
.has_key('Pre-install-command'):
918 if _cmd(output
, '/tmp', self
._dict
['Pre-install-command']):
919 return "pre-install %s: running \"%s\" failed" % \
920 (self
.fullname(), self
._dict
['Pre-install-command'])
924 installcmd
= self
._dict
.get('Install-command')
926 if '%' in installcmd
:
927 installcmd
= installcmd
% self
.archiveFilename
929 installcmd
= 'open \"%s\"' % self
.archiveFilename
930 if _cmd(output
, "/tmp", installcmd
):
931 return '%s: install command failed (use verbose for details)' % self
.fullname()
932 return '%s: downloaded and opened. Install manually and restart Package Manager' % self
.archiveFilename
935 """Installer engine: computes dependencies and installs
936 packages in the right order."""
938 def __init__(self
, db
):
942 self
._curmessages
= []
944 def __contains__(self
, package
):
945 return package
in self
._todo
947 def _addPackages(self
, packages
):
948 for package
in packages
:
949 if not package
in self
._todo
:
950 self
._todo
.append(package
)
952 def _prepareInstall(self
, package
, force
=0, recursive
=1):
953 """Internal routine, recursive engine for prepareInstall.
955 Test whether the package is installed and (if not installed
956 or if force==1) prepend it to the temporary todo list and
957 call ourselves recursively on all prerequisites."""
960 status
, message
= package
.installed()
963 if package
in self
._todo
or package
in self
._curtodo
:
965 self
._curtodo
.insert(0, package
)
968 prereqs
= package
.prerequisites()
969 for pkg
, descr
in prereqs
:
971 self
._prepareInstall
(pkg
, False, recursive
)
973 self
._curmessages
.append("Problem with dependency: %s" % descr
)
975 def prepareInstall(self
, package
, force
=0, recursive
=1):
976 """Prepare installation of a package.
978 If the package is already installed and force is false nothing
979 is done. If recursive is true prerequisites are installed first.
981 Returns a list of packages (to be passed to install) and a list
982 of messages of any problems encountered.
986 self
._curmessages
= []
987 self
._prepareInstall
(package
, force
, recursive
)
988 rv
= self
._curtodo
, self
._curmessages
990 self
._curmessages
= []
993 def install(self
, packages
, output
):
994 """Install a list of packages."""
996 self
._addPackages
(packages
)
998 for pkg
in self
._todo
:
999 msg
= pkg
.installSinglePackage(output
)
1006 def _run(mode
, verbose
, force
, args
, prefargs
, watcher
):
1007 """Engine for the main program"""
1009 prefs
= PimpPreferences(**prefargs
)
1011 prefs
.setWatcher(watcher
)
1014 sys
.stdout
.write(rv
)
1015 db
= PimpDatabase(prefs
)
1016 db
.appendURL(prefs
.pimpDatabase
)
1022 args
= db
.listnames()
1023 print "%-20.20s\t%s" % ("Package", "Description")
1025 for pkgname
in args
:
1026 pkg
= db
.find(pkgname
)
1028 description
= pkg
.shortdescription()
1029 pkgname
= pkg
.fullname()
1031 description
= 'Error: no such package'
1032 print "%-20.20s\t%s" % (pkgname
, description
)
1034 print "\tHome page:\t", pkg
.homepage()
1036 print "\tDownload URL:\t", pkg
.downloadURL()
1039 description
= pkg
.description()
1040 description
= '\n\t\t\t\t\t'.join(description
.splitlines())
1041 print "\tDescription:\t%s" % description
1042 elif mode
=='status':
1044 args
= db
.listnames()
1045 print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
1047 for pkgname
in args
:
1048 pkg
= db
.find(pkgname
)
1050 status
, msg
= pkg
.installed()
1051 pkgname
= pkg
.fullname()
1054 msg
= 'No such package'
1055 print "%-20.20s\t%-9.9s\t%s" % (pkgname
, status
, msg
)
1056 if verbose
and status
== "no":
1057 prereq
= pkg
.prerequisites()
1058 for pkg
, msg
in prereq
:
1062 pkg
= pkg
.fullname()
1063 print "%-20.20s\tRequirement: %s %s" % ("", pkg
, msg
)
1064 elif mode
== 'install':
1066 print 'Please specify packages to install'
1068 inst
= PimpInstaller(db
)
1069 for pkgname
in args
:
1070 pkg
= db
.find(pkgname
)
1072 print '%s: No such package' % pkgname
1074 list, messages
= inst
.prepareInstall(pkg
, force
)
1075 if messages
and not force
:
1076 print "%s: Not installed:" % pkgname
1084 messages
= inst
.install(list, output
)
1086 print "%s: Not installed:" % pkgname
1091 """Minimal commandline tool to drive pimp."""
1095 print "Usage: pimp [options] -s [package ...] List installed status"
1096 print " pimp [options] -l [package ...] Show package information"
1097 print " pimp [options] -i package ... Install packages"
1098 print " pimp -d Dump database to stdout"
1099 print " pimp -V Print version number"
1102 print " -f Force installation"
1103 print " -D dir Set destination directory"
1104 print " (default: %s)" % DEFAULT_INSTALLDIR
1105 print " -u url URL for database"
1109 def update(self
, msg
):
1110 sys
.stderr
.write(msg
+ '\r')
1114 opts
, args
= getopt
.getopt(sys
.argv
[1:], "slifvdD:Vu:")
1115 except getopt
.GetoptError
:
1117 if not opts
and not args
:
1147 watcher
= _Watcher()
1149 prefargs
['installDir'] = a
1151 prefargs
['pimpDatabase'] = a
1154 if mode
== 'version':
1155 print 'Pimp version %s; module name is %s' % (PIMP_VERSION
, __name__
)
1157 _run(mode
, verbose
, force
, args
, prefargs
, watcher
)
1159 # Finally, try to update ourselves to a newer version.
1160 # If the end-user updates pimp through pimp the new version
1161 # will be called pimp_update and live in site-packages
1162 # or somewhere similar
1163 if __name__
!= 'pimp_update':
1169 if pimp_update
.PIMP_VERSION
<= PIMP_VERSION
:
1171 warnings
.warn("pimp_update is version %s, not newer than pimp version %s" %
1172 (pimp_update
.PIMP_VERSION
, PIMP_VERSION
))
1174 from pimp_update
import *
1176 if __name__
== '__main__':