ยปCore Development>Code coverage>Lib/plat-mac/pimp.py

Python code coverage for Lib/plat-mac/pimp.py

#countcontent
1n/a"""Package Install Manager for Python.
2n/a
3n/aThis is currently a MacOSX-only strawman implementation.
4n/aDespite other rumours the name stands for "Packman IMPlementation".
5n/a
6n/aTools to allow easy installation of packages. The idea is that there is
7n/aan online XML database per (platform, python-version) containing packages
8n/aknown to work with that combination. This module contains tools for getting
9n/aand parsing the database, testing whether packages are installed, computing
10n/adependencies and installing packages.
11n/a
12n/aThere is a minimal main program that works as a command line tool, but the
13n/aintention is that the end user will use this through a GUI.
14n/a"""
15n/a
16n/afrom warnings import warnpy3k
17n/awarnpy3k("In 3.x, the pimp module is removed.", stacklevel=2)
18n/a
19n/aimport sys
20n/aimport os
21n/aimport subprocess
22n/aimport urllib
23n/aimport urllib2
24n/aimport urlparse
25n/aimport plistlib
26n/aimport distutils.util
27n/aimport distutils.sysconfig
28n/aimport hashlib
29n/aimport tarfile
30n/aimport tempfile
31n/aimport shutil
32n/aimport time
33n/a
34n/a__all__ = ["PimpPreferences", "PimpDatabase", "PimpPackage", "main",
35n/a "getDefaultDatabase", "PIMP_VERSION", "main"]
36n/a
37n/a_scriptExc_NotInstalled = "pimp._scriptExc_NotInstalled"
38n/a_scriptExc_OldInstalled = "pimp._scriptExc_OldInstalled"
39n/a_scriptExc_BadInstalled = "pimp._scriptExc_BadInstalled"
40n/a
41n/aNO_EXECUTE=0
42n/a
43n/aPIMP_VERSION="0.5"
44n/a
45n/a# Flavors:
46n/a# source: setup-based package
47n/a# binary: tar (or other) archive created with setup.py bdist.
48n/a# installer: something that can be opened
49n/aDEFAULT_FLAVORORDER=['source', 'binary', 'installer']
50n/aDEFAULT_DOWNLOADDIR='/tmp'
51n/aDEFAULT_BUILDDIR='/tmp'
52n/aDEFAULT_INSTALLDIR=distutils.sysconfig.get_python_lib()
53n/aDEFAULT_PIMPDATABASE_FMT="http://www.python.org/packman/version-%s/%s-%s-%s-%s-%s.plist"
54n/a
55n/adef getDefaultDatabase(experimental=False):
56n/a if experimental:
57n/a status = "exp"
58n/a else:
59n/a status = "prod"
60n/a
61n/a major, minor, micro, state, extra = sys.version_info
62n/a pyvers = '%d.%d' % (major, minor)
63n/a if micro == 0 and state != 'final':
64n/a pyvers = pyvers + '%s%d' % (state, extra)
65n/a
66n/a longplatform = distutils.util.get_platform()
67n/a osname, release, machine = longplatform.split('-')
68n/a # For some platforms we may want to differentiate between
69n/a # installation types
70n/a if osname == 'darwin':
71n/a if sys.prefix.startswith('/System/Library/Frameworks/Python.framework'):
72n/a osname = 'darwin_apple'
73n/a elif sys.prefix.startswith('/Library/Frameworks/Python.framework'):
74n/a osname = 'darwin_macpython'
75n/a # Otherwise we don't know...
76n/a # Now we try various URLs by playing with the release string.
77n/a # We remove numbers off the end until we find a match.
78n/a rel = release
79n/a while True:
80n/a url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, rel, machine)
81n/a try:
82n/a urllib2.urlopen(url)
83n/a except urllib2.HTTPError, arg:
84n/a pass
85n/a else:
86n/a break
87n/a if not rel:
88n/a # We're out of version numbers to try. Use the
89n/a # full release number, this will give a reasonable
90n/a # error message later
91n/a url = DEFAULT_PIMPDATABASE_FMT % (PIMP_VERSION, status, pyvers, osname, release, machine)
92n/a break
93n/a idx = rel.rfind('.')
94n/a if idx < 0:
95n/a rel = ''
96n/a else:
97n/a rel = rel[:idx]
98n/a return url
99n/a
100n/adef _cmd(output, dir, *cmditems):
101n/a """Internal routine to run a shell command in a given directory."""
102n/a
103n/a cmd = ("cd \"%s\"; " % dir) + " ".join(cmditems)
104n/a if output:
105n/a output.write("+ %s\n" % cmd)
106n/a if NO_EXECUTE:
107n/a return 0
108n/a child = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
109n/a stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
110n/a child.stdin.close()
111n/a while 1:
112n/a line = child.stdout.readline()
113n/a if not line:
114n/a break
115n/a if output:
116n/a output.write(line)
117n/a return child.wait()
118n/a
119n/aclass PimpDownloader:
120n/a """Abstract base class - Downloader for archives"""
121n/a
122n/a def __init__(self, argument,
123n/a dir="",
124n/a watcher=None):
125n/a self.argument = argument
126n/a self._dir = dir
127n/a self._watcher = watcher
128n/a
129n/a def download(self, url, filename, output=None):
130n/a return None
131n/a
132n/a def update(self, str):
133n/a if self._watcher:
134n/a return self._watcher.update(str)
135n/a return True
136n/a
137n/aclass PimpCurlDownloader(PimpDownloader):
138n/a
139n/a def download(self, url, filename, output=None):
140n/a self.update("Downloading %s..." % url)
141n/a exitstatus = _cmd(output, self._dir,
142n/a "curl",
143n/a "--output", filename,
144n/a url)
145n/a self.update("Downloading %s: finished" % url)
146n/a return (not exitstatus)
147n/a
148n/aclass PimpUrllibDownloader(PimpDownloader):
149n/a
150n/a def download(self, url, filename, output=None):
151n/a output = open(filename, 'wb')
152n/a self.update("Downloading %s: opening connection" % url)
153n/a keepgoing = True
154n/a download = urllib2.urlopen(url)
155n/a if 'content-length' in download.headers:
156n/a length = long(download.headers['content-length'])
157n/a else:
158n/a length = -1
159n/a
160n/a data = download.read(4096) #read 4K at a time
161n/a dlsize = 0
162n/a lasttime = 0
163n/a while keepgoing:
164n/a dlsize = dlsize + len(data)
165n/a if len(data) == 0:
166n/a #this is our exit condition
167n/a break
168n/a output.write(data)
169n/a if int(time.time()) != lasttime:
170n/a # Update at most once per second
171n/a lasttime = int(time.time())
172n/a if length == -1:
173n/a keepgoing = self.update("Downloading %s: %d bytes..." % (url, dlsize))
174n/a else:
175n/a keepgoing = self.update("Downloading %s: %d%% (%d bytes)..." % (url, int(100.0*dlsize/length), dlsize))
176n/a data = download.read(4096)
177n/a if keepgoing:
178n/a self.update("Downloading %s: finished" % url)
179n/a return keepgoing
180n/a
181n/aclass PimpUnpacker:
182n/a """Abstract base class - Unpacker for archives"""
183n/a
184n/a _can_rename = False
185n/a
186n/a def __init__(self, argument,
187n/a dir="",
188n/a renames=[],
189n/a watcher=None):
190n/a self.argument = argument
191n/a if renames and not self._can_rename:
192n/a raise RuntimeError, "This unpacker cannot rename files"
193n/a self._dir = dir
194n/a self._renames = renames
195n/a self._watcher = watcher
196n/a
197n/a def unpack(self, archive, output=None, package=None):
198n/a return None
199n/a
200n/a def update(self, str):
201n/a if self._watcher:
202n/a return self._watcher.update(str)
203n/a return True
204n/a
205n/aclass PimpCommandUnpacker(PimpUnpacker):
206n/a """Unpack archives by calling a Unix utility"""
207n/a
208n/a _can_rename = False
209n/a
210n/a def unpack(self, archive, output=None, package=None):
211n/a cmd = self.argument % archive
212n/a if _cmd(output, self._dir, cmd):
213n/a return "unpack command failed"
214n/a
215n/aclass PimpTarUnpacker(PimpUnpacker):
216n/a """Unpack tarfiles using the builtin tarfile module"""
217n/a
218n/a _can_rename = True
219n/a
220n/a def unpack(self, archive, output=None, package=None):
221n/a tf = tarfile.open(archive, "r")
222n/a members = tf.getmembers()
223n/a skip = []
224n/a if self._renames:
225n/a for member in members:
226n/a for oldprefix, newprefix in self._renames:
227n/a if oldprefix[:len(self._dir)] == self._dir:
228n/a oldprefix2 = oldprefix[len(self._dir):]
229n/a else:
230n/a oldprefix2 = None
231n/a if member.name[:len(oldprefix)] == oldprefix:
232n/a if newprefix is None:
233n/a skip.append(member)
234n/a #print 'SKIP', member.name
235n/a else:
236n/a member.name = newprefix + member.name[len(oldprefix):]
237n/a print ' ', member.name
238n/a break
239n/a elif oldprefix2 and member.name[:len(oldprefix2)] == oldprefix2:
240n/a if newprefix is None:
241n/a skip.append(member)
242n/a #print 'SKIP', member.name
243n/a else:
244n/a member.name = newprefix + member.name[len(oldprefix2):]
245n/a #print ' ', member.name
246n/a break
247n/a else:
248n/a skip.append(member)
249n/a #print '????', member.name
250n/a for member in members:
251n/a if member in skip:
252n/a self.update("Skipping %s" % member.name)
253n/a continue
254n/a self.update("Extracting %s" % member.name)
255n/a tf.extract(member, self._dir)
256n/a if skip:
257n/a names = [member.name for member in skip if member.name[-1] != '/']
258n/a if package:
259n/a names = package.filterExpectedSkips(names)
260n/a if names:
261n/a return "Not all files were unpacked: %s" % " ".join(names)
262n/a
263n/aARCHIVE_FORMATS = [
264n/a (".tar.Z", PimpTarUnpacker, None),
265n/a (".taz", PimpTarUnpacker, None),
266n/a (".tar.gz", PimpTarUnpacker, None),
267n/a (".tgz", PimpTarUnpacker, None),
268n/a (".tar.bz", PimpTarUnpacker, None),
269n/a (".zip", PimpCommandUnpacker, "unzip \"%s\""),
270n/a]
271n/a
272n/aclass PimpPreferences:
273n/a """Container for per-user preferences, such as the database to use
274n/a and where to install packages."""
275n/a
276n/a def __init__(self,
277n/a flavorOrder=None,
278n/a downloadDir=None,
279n/a buildDir=None,
280n/a installDir=None,
281n/a pimpDatabase=None):
282n/a if not flavorOrder:
283n/a flavorOrder = DEFAULT_FLAVORORDER
284n/a if not downloadDir:
285n/a downloadDir = DEFAULT_DOWNLOADDIR
286n/a if not buildDir:
287n/a buildDir = DEFAULT_BUILDDIR
288n/a if not pimpDatabase:
289n/a pimpDatabase = getDefaultDatabase()
290n/a self.setInstallDir(installDir)
291n/a self.flavorOrder = flavorOrder
292n/a self.downloadDir = downloadDir
293n/a self.buildDir = buildDir
294n/a self.pimpDatabase = pimpDatabase
295n/a self.watcher = None
296n/a
297n/a def setWatcher(self, watcher):
298n/a self.watcher = watcher
299n/a
300n/a def setInstallDir(self, installDir=None):
301n/a if installDir:
302n/a # Installing to non-standard location.
303n/a self.installLocations = [
304n/a ('--install-lib', installDir),
305n/a ('--install-headers', None),
306n/a ('--install-scripts', None),
307n/a ('--install-data', None)]
308n/a else:
309n/a installDir = DEFAULT_INSTALLDIR
310n/a self.installLocations = []
311n/a self.installDir = installDir
312n/a
313n/a def isUserInstall(self):
314n/a return self.installDir != DEFAULT_INSTALLDIR
315n/a
316n/a def check(self):
317n/a """Check that the preferences make sense: directories exist and are
318n/a writable, the install directory is on sys.path, etc."""
319n/a
320n/a rv = ""
321n/a RWX_OK = os.R_OK|os.W_OK|os.X_OK
322n/a if not os.path.exists(self.downloadDir):
323n/a rv += "Warning: Download directory \"%s\" does not exist\n" % self.downloadDir
324n/a elif not os.access(self.downloadDir, RWX_OK):
325n/a rv += "Warning: Download directory \"%s\" is not writable or not readable\n" % self.downloadDir
326n/a if not os.path.exists(self.buildDir):
327n/a rv += "Warning: Build directory \"%s\" does not exist\n" % self.buildDir
328n/a elif not os.access(self.buildDir, RWX_OK):
329n/a rv += "Warning: Build directory \"%s\" is not writable or not readable\n" % self.buildDir
330n/a if not os.path.exists(self.installDir):
331n/a rv += "Warning: Install directory \"%s\" does not exist\n" % self.installDir
332n/a elif not os.access(self.installDir, RWX_OK):
333n/a rv += "Warning: Install directory \"%s\" is not writable or not readable\n" % self.installDir
334n/a else:
335n/a installDir = os.path.realpath(self.installDir)
336n/a for p in sys.path:
337n/a try:
338n/a realpath = os.path.realpath(p)
339n/a except:
340n/a pass
341n/a if installDir == realpath:
342n/a break
343n/a else:
344n/a rv += "Warning: Install directory \"%s\" is not on sys.path\n" % self.installDir
345n/a return rv
346n/a
347n/a def compareFlavors(self, left, right):
348n/a """Compare two flavor strings. This is part of your preferences
349n/a because whether the user prefers installing from source or binary is."""
350n/a if left in self.flavorOrder:
351n/a if right in self.flavorOrder:
352n/a return cmp(self.flavorOrder.index(left), self.flavorOrder.index(right))
353n/a return -1
354n/a if right in self.flavorOrder:
355n/a return 1
356n/a return cmp(left, right)
357n/a
358n/aclass PimpDatabase:
359n/a """Class representing a pimp database. It can actually contain
360n/a information from multiple databases through inclusion, but the
361n/a toplevel database is considered the master, as its maintainer is
362n/a "responsible" for the contents."""
363n/a
364n/a def __init__(self, prefs):
365n/a self._packages = []
366n/a self.preferences = prefs
367n/a self._url = ""
368n/a self._urllist = []
369n/a self._version = ""
370n/a self._maintainer = ""
371n/a self._description = ""
372n/a
373n/a # Accessor functions
374n/a def url(self): return self._url
375n/a def version(self): return self._version
376n/a def maintainer(self): return self._maintainer
377n/a def description(self): return self._description
378n/a
379n/a def close(self):
380n/a """Clean up"""
381n/a self._packages = []
382n/a self.preferences = None
383n/a
384n/a def appendURL(self, url, included=0):
385n/a """Append packages from the database with the given URL.
386n/a Only the first database should specify included=0, so the
387n/a global information (maintainer, description) get stored."""
388n/a
389n/a if url in self._urllist:
390n/a return
391n/a self._urllist.append(url)
392n/a fp = urllib2.urlopen(url).fp
393n/a plistdata = plistlib.Plist.fromFile(fp)
394n/a # Test here for Pimp version, etc
395n/a if included:
396n/a version = plistdata.get('Version')
397n/a if version and version > self._version:
398n/a sys.stderr.write("Warning: included database %s is for pimp version %s\n" %
399n/a (url, version))
400n/a else:
401n/a self._version = plistdata.get('Version')
402n/a if not self._version:
403n/a sys.stderr.write("Warning: database has no Version information\n")
404n/a elif self._version > PIMP_VERSION:
405n/a sys.stderr.write("Warning: database version %s newer than pimp version %s\n"
406n/a % (self._version, PIMP_VERSION))
407n/a self._maintainer = plistdata.get('Maintainer', '')
408n/a self._description = plistdata.get('Description', '').strip()
409n/a self._url = url
410n/a self._appendPackages(plistdata['Packages'], url)
411n/a others = plistdata.get('Include', [])
412n/a for o in others:
413n/a o = urllib.basejoin(url, o)
414n/a self.appendURL(o, included=1)
415n/a
416n/a def _appendPackages(self, packages, url):
417n/a """Given a list of dictionaries containing package
418n/a descriptions create the PimpPackage objects and append them
419n/a to our internal storage."""
420n/a
421n/a for p in packages:
422n/a p = dict(p)
423n/a if 'Download-URL' in p:
424n/a p['Download-URL'] = urllib.basejoin(url, p['Download-URL'])
425n/a flavor = p.get('Flavor')
426n/a if flavor == 'source':
427n/a pkg = PimpPackage_source(self, p)
428n/a elif flavor == 'binary':
429n/a pkg = PimpPackage_binary(self, p)
430n/a elif flavor == 'installer':
431n/a pkg = PimpPackage_installer(self, p)
432n/a elif flavor == 'hidden':
433n/a pkg = PimpPackage_installer(self, p)
434n/a else:
435n/a pkg = PimpPackage(self, dict(p))
436n/a self._packages.append(pkg)
437n/a
438n/a def list(self):
439n/a """Return a list of all PimpPackage objects in the database."""
440n/a
441n/a return self._packages
442n/a
443n/a def listnames(self):
444n/a """Return a list of names of all packages in the database."""
445n/a
446n/a rv = []
447n/a for pkg in self._packages:
448n/a rv.append(pkg.fullname())
449n/a rv.sort()
450n/a return rv
451n/a
452n/a def dump(self, pathOrFile):
453n/a """Dump the contents of the database to an XML .plist file.
454n/a
455n/a The file can be passed as either a file object or a pathname.
456n/a All data, including included databases, is dumped."""
457n/a
458n/a packages = []
459n/a for pkg in self._packages:
460n/a packages.append(pkg.dump())
461n/a plistdata = {
462n/a 'Version': self._version,
463n/a 'Maintainer': self._maintainer,
464n/a 'Description': self._description,
465n/a 'Packages': packages
466n/a }
467n/a plist = plistlib.Plist(**plistdata)
468n/a plist.write(pathOrFile)
469n/a
470n/a def find(self, ident):
471n/a """Find a package. The package can be specified by name
472n/a or as a dictionary with name, version and flavor entries.
473n/a
474n/a Only name is obligatory. If there are multiple matches the
475n/a best one (higher version number, flavors ordered according to
476n/a users' preference) is returned."""
477n/a
478n/a if type(ident) == str:
479n/a # Remove ( and ) for pseudo-packages
480n/a if ident[0] == '(' and ident[-1] == ')':
481n/a ident = ident[1:-1]
482n/a # Split into name-version-flavor
483n/a fields = ident.split('-')
484n/a if len(fields) < 1 or len(fields) > 3:
485n/a return None
486n/a name = fields[0]
487n/a if len(fields) > 1:
488n/a version = fields[1]
489n/a else:
490n/a version = None
491n/a if len(fields) > 2:
492n/a flavor = fields[2]
493n/a else:
494n/a flavor = None
495n/a else:
496n/a name = ident['Name']
497n/a version = ident.get('Version')
498n/a flavor = ident.get('Flavor')
499n/a found = None
500n/a for p in self._packages:
501n/a if name == p.name() and \
502n/a (not version or version == p.version()) and \
503n/a (not flavor or flavor == p.flavor()):
504n/a if not found or found < p:
505n/a found = p
506n/a return found
507n/a
508n/aALLOWED_KEYS = [
509n/a "Name",
510n/a "Version",
511n/a "Flavor",
512n/a "Description",
513n/a "Home-page",
514n/a "Download-URL",
515n/a "Install-test",
516n/a "Install-command",
517n/a "Pre-install-command",
518n/a "Post-install-command",
519n/a "Prerequisites",
520n/a "MD5Sum",
521n/a "User-install-skips",
522n/a "Systemwide-only",
523n/a]
524n/a
525n/aclass PimpPackage:
526n/a """Class representing a single package."""
527n/a
528n/a def __init__(self, db, plistdata):
529n/a self._db = db
530n/a name = plistdata["Name"]
531n/a for k in plistdata.keys():
532n/a if not k in ALLOWED_KEYS:
533n/a sys.stderr.write("Warning: %s: unknown key %s\n" % (name, k))
534n/a self._dict = plistdata
535n/a
536n/a def __getitem__(self, key):
537n/a return self._dict[key]
538n/a
539n/a def name(self): return self._dict['Name']
540n/a def version(self): return self._dict.get('Version')
541n/a def flavor(self): return self._dict.get('Flavor')
542n/a def description(self): return self._dict['Description'].strip()
543n/a def shortdescription(self): return self.description().splitlines()[0]
544n/a def homepage(self): return self._dict.get('Home-page')
545n/a def downloadURL(self): return self._dict.get('Download-URL')
546n/a def systemwideOnly(self): return self._dict.get('Systemwide-only')
547n/a
548n/a def fullname(self):
549n/a """Return the full name "name-version-flavor" of a package.
550n/a
551n/a If the package is a pseudo-package, something that cannot be
552n/a installed through pimp, return the name in (parentheses)."""
553n/a
554n/a rv = self._dict['Name']
555n/a if 'Version' in self._dict:
556n/a rv = rv + '-%s' % self._dict['Version']
557n/a if 'Flavor' in self._dict:
558n/a rv = rv + '-%s' % self._dict['Flavor']
559n/a if self._dict.get('Flavor') == 'hidden':
560n/a # Pseudo-package, show in parentheses
561n/a rv = '(%s)' % rv
562n/a return rv
563n/a
564n/a def dump(self):
565n/a """Return a dict object containing the information on the package."""
566n/a return self._dict
567n/a
568n/a def __cmp__(self, other):
569n/a """Compare two packages, where the "better" package sorts lower."""
570n/a
571n/a if not isinstance(other, PimpPackage):
572n/a return cmp(id(self), id(other))
573n/a if self.name() != other.name():
574n/a return cmp(self.name(), other.name())
575n/a if self.version() != other.version():
576n/a return -cmp(self.version(), other.version())
577n/a return self._db.preferences.compareFlavors(self.flavor(), other.flavor())
578n/a
579n/a def installed(self):
580n/a """Test wheter the package is installed.
581n/a
582n/a Returns two values: a status indicator which is one of
583n/a "yes", "no", "old" (an older version is installed) or "bad"
584n/a (something went wrong during the install test) and a human
585n/a readable string which may contain more details."""
586n/a
587n/a namespace = {
588n/a "NotInstalled": _scriptExc_NotInstalled,
589n/a "OldInstalled": _scriptExc_OldInstalled,
590n/a "BadInstalled": _scriptExc_BadInstalled,
591n/a "os": os,
592n/a "sys": sys,
593n/a }
594n/a installTest = self._dict['Install-test'].strip() + '\n'
595n/a try:
596n/a exec installTest in namespace
597n/a except ImportError, arg:
598n/a return "no", str(arg)
599n/a except _scriptExc_NotInstalled, arg:
600n/a return "no", str(arg)
601n/a except _scriptExc_OldInstalled, arg:
602n/a return "old", str(arg)
603n/a except _scriptExc_BadInstalled, arg:
604n/a return "bad", str(arg)
605n/a except:
606n/a sys.stderr.write("-------------------------------------\n")
607n/a sys.stderr.write("---- %s: install test got exception\n" % self.fullname())
608n/a sys.stderr.write("---- source:\n")
609n/a sys.stderr.write(installTest)
610n/a sys.stderr.write("---- exception:\n")
611n/a import traceback
612n/a traceback.print_exc(file=sys.stderr)
613n/a if self._db._maintainer:
614n/a sys.stderr.write("---- Please copy this and mail to %s\n" % self._db._maintainer)
615n/a sys.stderr.write("-------------------------------------\n")
616n/a return "bad", "Package install test got exception"
617n/a return "yes", ""
618n/a
619n/a def prerequisites(self):
620n/a """Return a list of prerequisites for this package.
621n/a
622n/a The list contains 2-tuples, of which the first item is either
623n/a a PimpPackage object or None, and the second is a descriptive
624n/a string. The first item can be None if this package depends on
625n/a something that isn't pimp-installable, in which case the descriptive
626n/a string should tell the user what to do."""
627n/a
628n/a rv = []
629n/a if not self._dict.get('Download-URL'):
630n/a # For pseudo-packages that are already installed we don't
631n/a # return an error message
632n/a status, _ = self.installed()
633n/a if status == "yes":
634n/a return []
635n/a return [(None,
636n/a "Package %s cannot be installed automatically, see the description" %
637n/a self.fullname())]
638n/a if self.systemwideOnly() and self._db.preferences.isUserInstall():
639n/a return [(None,
640n/a "Package %s can only be installed system-wide" %
641n/a self.fullname())]
642n/a if not self._dict.get('Prerequisites'):
643n/a return []
644n/a for item in self._dict['Prerequisites']:
645n/a if type(item) == str:
646n/a pkg = None
647n/a descr = str(item)
648n/a else:
649n/a name = item['Name']
650n/a if 'Version' in item:
651n/a name = name + '-' + item['Version']
652n/a if 'Flavor' in item:
653n/a name = name + '-' + item['Flavor']
654n/a pkg = self._db.find(name)
655n/a if not pkg:
656n/a descr = "Requires unknown %s"%name
657n/a else:
658n/a descr = pkg.shortdescription()
659n/a rv.append((pkg, descr))
660n/a return rv
661n/a
662n/a
663n/a def downloadPackageOnly(self, output=None):
664n/a """Download a single package, if needed.
665n/a
666n/a An MD5 signature is used to determine whether download is needed,
667n/a and to test that we actually downloaded what we expected.
668n/a If output is given it is a file-like object that will receive a log
669n/a of what happens.
670n/a
671n/a If anything unforeseen happened the method returns an error message
672n/a string.
673n/a """
674n/a
675n/a scheme, loc, path, query, frag = urlparse.urlsplit(self._dict['Download-URL'])
676n/a path = urllib.url2pathname(path)
677n/a filename = os.path.split(path)[1]
678n/a self.archiveFilename = os.path.join(self._db.preferences.downloadDir, filename)
679n/a if not self._archiveOK():
680n/a if scheme == 'manual':
681n/a return "Please download package manually and save as %s" % self.archiveFilename
682n/a downloader = PimpUrllibDownloader(None, self._db.preferences.downloadDir,
683n/a watcher=self._db.preferences.watcher)
684n/a if not downloader.download(self._dict['Download-URL'],
685n/a self.archiveFilename, output):
686n/a return "download command failed"
687n/a if not os.path.exists(self.archiveFilename) and not NO_EXECUTE:
688n/a return "archive not found after download"
689n/a if not self._archiveOK():
690n/a return "archive does not have correct MD5 checksum"
691n/a
692n/a def _archiveOK(self):
693n/a """Test an archive. It should exist and the MD5 checksum should be correct."""
694n/a
695n/a if not os.path.exists(self.archiveFilename):
696n/a return 0
697n/a if not self._dict.get('MD5Sum'):
698n/a sys.stderr.write("Warning: no MD5Sum for %s\n" % self.fullname())
699n/a return 1
700n/a data = open(self.archiveFilename, 'rb').read()
701n/a checksum = hashlib.md5(data).hexdigest()
702n/a return checksum == self._dict['MD5Sum']
703n/a
704n/a def unpackPackageOnly(self, output=None):
705n/a """Unpack a downloaded package archive."""
706n/a
707n/a filename = os.path.split(self.archiveFilename)[1]
708n/a for ext, unpackerClass, arg in ARCHIVE_FORMATS:
709n/a if filename[-len(ext):] == ext:
710n/a break
711n/a else:
712n/a return "unknown extension for archive file: %s" % filename
713n/a self.basename = filename[:-len(ext)]
714n/a unpacker = unpackerClass(arg, dir=self._db.preferences.buildDir,
715n/a watcher=self._db.preferences.watcher)
716n/a rv = unpacker.unpack(self.archiveFilename, output=output)
717n/a if rv:
718n/a return rv
719n/a
720n/a def installPackageOnly(self, output=None):
721n/a """Default install method, to be overridden by subclasses"""
722n/a return "%s: This package needs to be installed manually (no support for flavor=\"%s\")" \
723n/a % (self.fullname(), self._dict.get(flavor, ""))
724n/a
725n/a def installSinglePackage(self, output=None):
726n/a """Download, unpack and install a single package.
727n/a
728n/a If output is given it should be a file-like object and it
729n/a will receive a log of what happened."""
730n/a
731n/a if not self._dict.get('Download-URL'):
732n/a return "%s: This package needs to be installed manually (no Download-URL field)" % self.fullname()
733n/a msg = self.downloadPackageOnly(output)
734n/a if msg:
735n/a return "%s: download: %s" % (self.fullname(), msg)
736n/a
737n/a msg = self.unpackPackageOnly(output)
738n/a if msg:
739n/a return "%s: unpack: %s" % (self.fullname(), msg)
740n/a
741n/a return self.installPackageOnly(output)
742n/a
743n/a def beforeInstall(self):
744n/a """Bookkeeping before installation: remember what we have in site-packages"""
745n/a self._old_contents = os.listdir(self._db.preferences.installDir)
746n/a
747n/a def afterInstall(self):
748n/a """Bookkeeping after installation: interpret any new .pth files that have
749n/a appeared"""
750n/a
751n/a new_contents = os.listdir(self._db.preferences.installDir)
752n/a for fn in new_contents:
753n/a if fn in self._old_contents:
754n/a continue
755n/a if fn[-4:] != '.pth':
756n/a continue
757n/a fullname = os.path.join(self._db.preferences.installDir, fn)
758n/a f = open(fullname)
759n/a for line in f.readlines():
760n/a if not line:
761n/a continue
762n/a if line[0] == '#':
763n/a continue
764n/a if line[:6] == 'import':
765n/a exec line
766n/a continue
767n/a if line[-1] == '\n':
768n/a line = line[:-1]
769n/a if not os.path.isabs(line):
770n/a line = os.path.join(self._db.preferences.installDir, line)
771n/a line = os.path.realpath(line)
772n/a if not line in sys.path:
773n/a sys.path.append(line)
774n/a
775n/a def filterExpectedSkips(self, names):
776n/a """Return a list that contains only unpexpected skips"""
777n/a if not self._db.preferences.isUserInstall():
778n/a return names
779n/a expected_skips = self._dict.get('User-install-skips')
780n/a if not expected_skips:
781n/a return names
782n/a newnames = []
783n/a for name in names:
784n/a for skip in expected_skips:
785n/a if name[:len(skip)] == skip:
786n/a break
787n/a else:
788n/a newnames.append(name)
789n/a return newnames
790n/a
791n/aclass PimpPackage_binary(PimpPackage):
792n/a
793n/a def unpackPackageOnly(self, output=None):
794n/a """We don't unpack binary packages until installing"""
795n/a pass
796n/a
797n/a def installPackageOnly(self, output=None):
798n/a """Install a single source package.
799n/a
800n/a If output is given it should be a file-like object and it
801n/a will receive a log of what happened."""
802n/a
803n/a if 'Install-command' in self._dict:
804n/a return "%s: Binary package cannot have Install-command" % self.fullname()
805n/a
806n/a if 'Pre-install-command' in self._dict:
807n/a if _cmd(output, '/tmp', self._dict['Pre-install-command']):
808n/a return "pre-install %s: running \"%s\" failed" % \
809n/a (self.fullname(), self._dict['Pre-install-command'])
810n/a
811n/a self.beforeInstall()
812n/a
813n/a # Install by unpacking
814n/a filename = os.path.split(self.archiveFilename)[1]
815n/a for ext, unpackerClass, arg in ARCHIVE_FORMATS:
816n/a if filename[-len(ext):] == ext:
817n/a break
818n/a else:
819n/a return "%s: unknown extension for archive file: %s" % (self.fullname(), filename)
820n/a self.basename = filename[:-len(ext)]
821n/a
822n/a install_renames = []
823n/a for k, newloc in self._db.preferences.installLocations:
824n/a if not newloc:
825n/a continue
826n/a if k == "--install-lib":
827n/a oldloc = DEFAULT_INSTALLDIR
828n/a else:
829n/a return "%s: Don't know installLocation %s" % (self.fullname(), k)
830n/a install_renames.append((oldloc, newloc))
831n/a
832n/a unpacker = unpackerClass(arg, dir="/", renames=install_renames)
833n/a rv = unpacker.unpack(self.archiveFilename, output=output, package=self)
834n/a if rv:
835n/a return rv
836n/a
837n/a self.afterInstall()
838n/a
839n/a if 'Post-install-command' in self._dict:
840n/a if _cmd(output, '/tmp', self._dict['Post-install-command']):
841n/a return "%s: post-install: running \"%s\" failed" % \
842n/a (self.fullname(), self._dict['Post-install-command'])
843n/a
844n/a return None
845n/a
846n/a
847n/aclass PimpPackage_source(PimpPackage):
848n/a
849n/a def unpackPackageOnly(self, output=None):
850n/a """Unpack a source package and check that setup.py exists"""
851n/a PimpPackage.unpackPackageOnly(self, output)
852n/a # Test that a setup script has been create
853n/a self._buildDirname = os.path.join(self._db.preferences.buildDir, self.basename)
854n/a setupname = os.path.join(self._buildDirname, "setup.py")
855n/a if not os.path.exists(setupname) and not NO_EXECUTE:
856n/a return "no setup.py found after unpack of archive"
857n/a
858n/a def installPackageOnly(self, output=None):
859n/a """Install a single source package.
860n/a
861n/a If output is given it should be a file-like object and it
862n/a will receive a log of what happened."""
863n/a
864n/a if 'Pre-install-command' in self._dict:
865n/a if _cmd(output, self._buildDirname, self._dict['Pre-install-command']):
866n/a return "pre-install %s: running \"%s\" failed" % \
867n/a (self.fullname(), self._dict['Pre-install-command'])
868n/a
869n/a self.beforeInstall()
870n/a installcmd = self._dict.get('Install-command')
871n/a if installcmd and self._install_renames:
872n/a return "Package has install-command and can only be installed to standard location"
873n/a # This is the "bit-bucket" for installations: everything we don't
874n/a # want. After installation we check that it is actually empty
875n/a unwanted_install_dir = None
876n/a if not installcmd:
877n/a extra_args = ""
878n/a for k, v in self._db.preferences.installLocations:
879n/a if not v:
880n/a # We don't want these files installed. Send them
881n/a # to the bit-bucket.
882n/a if not unwanted_install_dir:
883n/a unwanted_install_dir = tempfile.mkdtemp()
884n/a v = unwanted_install_dir
885n/a extra_args = extra_args + " %s \"%s\"" % (k, v)
886n/a installcmd = '"%s" setup.py install %s' % (sys.executable, extra_args)
887n/a if _cmd(output, self._buildDirname, installcmd):
888n/a return "install %s: running \"%s\" failed" % \
889n/a (self.fullname(), installcmd)
890n/a if unwanted_install_dir and os.path.exists(unwanted_install_dir):
891n/a unwanted_files = os.listdir(unwanted_install_dir)
892n/a if unwanted_files:
893n/a rv = "Warning: some files were not installed: %s" % " ".join(unwanted_files)
894n/a else:
895n/a rv = None
896n/a shutil.rmtree(unwanted_install_dir)
897n/a return rv
898n/a
899n/a self.afterInstall()
900n/a
901n/a if 'Post-install-command' in self._dict:
902n/a if _cmd(output, self._buildDirname, self._dict['Post-install-command']):
903n/a return "post-install %s: running \"%s\" failed" % \
904n/a (self.fullname(), self._dict['Post-install-command'])
905n/a return None
906n/a
907n/aclass PimpPackage_installer(PimpPackage):
908n/a
909n/a def unpackPackageOnly(self, output=None):
910n/a """We don't unpack dmg packages until installing"""
911n/a pass
912n/a
913n/a def installPackageOnly(self, output=None):
914n/a """Install a single source package.
915n/a
916n/a If output is given it should be a file-like object and it
917n/a will receive a log of what happened."""
918n/a
919n/a if 'Post-install-command' in self._dict:
920n/a return "%s: Installer package cannot have Post-install-command" % self.fullname()
921n/a
922n/a if 'Pre-install-command' in self._dict:
923n/a if _cmd(output, '/tmp', self._dict['Pre-install-command']):
924n/a return "pre-install %s: running \"%s\" failed" % \
925n/a (self.fullname(), self._dict['Pre-install-command'])
926n/a
927n/a self.beforeInstall()
928n/a
929n/a installcmd = self._dict.get('Install-command')
930n/a if installcmd:
931n/a if '%' in installcmd:
932n/a installcmd = installcmd % self.archiveFilename
933n/a else:
934n/a installcmd = 'open \"%s\"' % self.archiveFilename
935n/a if _cmd(output, "/tmp", installcmd):
936n/a return '%s: install command failed (use verbose for details)' % self.fullname()
937n/a return '%s: downloaded and opened. Install manually and restart Package Manager' % self.archiveFilename
938n/a
939n/aclass PimpInstaller:
940n/a """Installer engine: computes dependencies and installs
941n/a packages in the right order."""
942n/a
943n/a def __init__(self, db):
944n/a self._todo = []
945n/a self._db = db
946n/a self._curtodo = []
947n/a self._curmessages = []
948n/a
949n/a def __contains__(self, package):
950n/a return package in self._todo
951n/a
952n/a def _addPackages(self, packages):
953n/a for package in packages:
954n/a if not package in self._todo:
955n/a self._todo.append(package)
956n/a
957n/a def _prepareInstall(self, package, force=0, recursive=1):
958n/a """Internal routine, recursive engine for prepareInstall.
959n/a
960n/a Test whether the package is installed and (if not installed
961n/a or if force==1) prepend it to the temporary todo list and
962n/a call ourselves recursively on all prerequisites."""
963n/a
964n/a if not force:
965n/a status, message = package.installed()
966n/a if status == "yes":
967n/a return
968n/a if package in self._todo or package in self._curtodo:
969n/a return
970n/a self._curtodo.insert(0, package)
971n/a if not recursive:
972n/a return
973n/a prereqs = package.prerequisites()
974n/a for pkg, descr in prereqs:
975n/a if pkg:
976n/a self._prepareInstall(pkg, False, recursive)
977n/a else:
978n/a self._curmessages.append("Problem with dependency: %s" % descr)
979n/a
980n/a def prepareInstall(self, package, force=0, recursive=1):
981n/a """Prepare installation of a package.
982n/a
983n/a If the package is already installed and force is false nothing
984n/a is done. If recursive is true prerequisites are installed first.
985n/a
986n/a Returns a list of packages (to be passed to install) and a list
987n/a of messages of any problems encountered.
988n/a """
989n/a
990n/a self._curtodo = []
991n/a self._curmessages = []
992n/a self._prepareInstall(package, force, recursive)
993n/a rv = self._curtodo, self._curmessages
994n/a self._curtodo = []
995n/a self._curmessages = []
996n/a return rv
997n/a
998n/a def install(self, packages, output):
999n/a """Install a list of packages."""
1000n/a
1001n/a self._addPackages(packages)
1002n/a status = []
1003n/a for pkg in self._todo:
1004n/a msg = pkg.installSinglePackage(output)
1005n/a if msg:
1006n/a status.append(msg)
1007n/a return status
1008n/a
1009n/a
1010n/a
1011n/adef _run(mode, verbose, force, args, prefargs, watcher):
1012n/a """Engine for the main program"""
1013n/a
1014n/a prefs = PimpPreferences(**prefargs)
1015n/a if watcher:
1016n/a prefs.setWatcher(watcher)
1017n/a rv = prefs.check()
1018n/a if rv:
1019n/a sys.stdout.write(rv)
1020n/a db = PimpDatabase(prefs)
1021n/a db.appendURL(prefs.pimpDatabase)
1022n/a
1023n/a if mode == 'dump':
1024n/a db.dump(sys.stdout)
1025n/a elif mode =='list':
1026n/a if not args:
1027n/a args = db.listnames()
1028n/a print "%-20.20s\t%s" % ("Package", "Description")
1029n/a print
1030n/a for pkgname in args:
1031n/a pkg = db.find(pkgname)
1032n/a if pkg:
1033n/a description = pkg.shortdescription()
1034n/a pkgname = pkg.fullname()
1035n/a else:
1036n/a description = 'Error: no such package'
1037n/a print "%-20.20s\t%s" % (pkgname, description)
1038n/a if verbose:
1039n/a print "\tHome page:\t", pkg.homepage()
1040n/a try:
1041n/a print "\tDownload URL:\t", pkg.downloadURL()
1042n/a except KeyError:
1043n/a pass
1044n/a description = pkg.description()
1045n/a description = '\n\t\t\t\t\t'.join(description.splitlines())
1046n/a print "\tDescription:\t%s" % description
1047n/a elif mode =='status':
1048n/a if not args:
1049n/a args = db.listnames()
1050n/a print "%-20.20s\t%s\t%s" % ("Package", "Installed", "Message")
1051n/a print
1052n/a for pkgname in args:
1053n/a pkg = db.find(pkgname)
1054n/a if pkg:
1055n/a status, msg = pkg.installed()
1056n/a pkgname = pkg.fullname()
1057n/a else:
1058n/a status = 'error'
1059n/a msg = 'No such package'
1060n/a print "%-20.20s\t%-9.9s\t%s" % (pkgname, status, msg)
1061n/a if verbose and status == "no":
1062n/a prereq = pkg.prerequisites()
1063n/a for pkg, msg in prereq:
1064n/a if not pkg:
1065n/a pkg = ''
1066n/a else:
1067n/a pkg = pkg.fullname()
1068n/a print "%-20.20s\tRequirement: %s %s" % ("", pkg, msg)
1069n/a elif mode == 'install':
1070n/a if not args:
1071n/a print 'Please specify packages to install'
1072n/a sys.exit(1)
1073n/a inst = PimpInstaller(db)
1074n/a for pkgname in args:
1075n/a pkg = db.find(pkgname)
1076n/a if not pkg:
1077n/a print '%s: No such package' % pkgname
1078n/a continue
1079n/a list, messages = inst.prepareInstall(pkg, force)
1080n/a if messages and not force:
1081n/a print "%s: Not installed:" % pkgname
1082n/a for m in messages:
1083n/a print "\t", m
1084n/a else:
1085n/a if verbose:
1086n/a output = sys.stdout
1087n/a else:
1088n/a output = None
1089n/a messages = inst.install(list, output)
1090n/a if messages:
1091n/a print "%s: Not installed:" % pkgname
1092n/a for m in messages:
1093n/a print "\t", m
1094n/a
1095n/adef main():
1096n/a """Minimal commandline tool to drive pimp."""
1097n/a
1098n/a import getopt
1099n/a def _help():
1100n/a print "Usage: pimp [options] -s [package ...] List installed status"
1101n/a print " pimp [options] -l [package ...] Show package information"
1102n/a print " pimp [options] -i package ... Install packages"
1103n/a print " pimp -d Dump database to stdout"
1104n/a print " pimp -V Print version number"
1105n/a print "Options:"
1106n/a print " -v Verbose"
1107n/a print " -f Force installation"
1108n/a print " -D dir Set destination directory"
1109n/a print " (default: %s)" % DEFAULT_INSTALLDIR
1110n/a print " -u url URL for database"
1111n/a sys.exit(1)
1112n/a
1113n/a class _Watcher:
1114n/a def update(self, msg):
1115n/a sys.stderr.write(msg + '\r')
1116n/a return 1
1117n/a
1118n/a try:
1119n/a opts, args = getopt.getopt(sys.argv[1:], "slifvdD:Vu:")
1120n/a except getopt.GetoptError:
1121n/a _help()
1122n/a if not opts and not args:
1123n/a _help()
1124n/a mode = None
1125n/a force = 0
1126n/a verbose = 0
1127n/a prefargs = {}
1128n/a watcher = None
1129n/a for o, a in opts:
1130n/a if o == '-s':
1131n/a if mode:
1132n/a _help()
1133n/a mode = 'status'
1134n/a if o == '-l':
1135n/a if mode:
1136n/a _help()
1137n/a mode = 'list'
1138n/a if o == '-d':
1139n/a if mode:
1140n/a _help()
1141n/a mode = 'dump'
1142n/a if o == '-V':
1143n/a if mode:
1144n/a _help()
1145n/a mode = 'version'
1146n/a if o == '-i':
1147n/a mode = 'install'
1148n/a if o == '-f':
1149n/a force = 1
1150n/a if o == '-v':
1151n/a verbose = 1
1152n/a watcher = _Watcher()
1153n/a if o == '-D':
1154n/a prefargs['installDir'] = a
1155n/a if o == '-u':
1156n/a prefargs['pimpDatabase'] = a
1157n/a if not mode:
1158n/a _help()
1159n/a if mode == 'version':
1160n/a print 'Pimp version %s; module name is %s' % (PIMP_VERSION, __name__)
1161n/a else:
1162n/a _run(mode, verbose, force, args, prefargs, watcher)
1163n/a
1164n/a# Finally, try to update ourselves to a newer version.
1165n/a# If the end-user updates pimp through pimp the new version
1166n/a# will be called pimp_update and live in site-packages
1167n/a# or somewhere similar
1168n/aif __name__ != 'pimp_update':
1169n/a try:
1170n/a import pimp_update
1171n/a except ImportError:
1172n/a pass
1173n/a else:
1174n/a if pimp_update.PIMP_VERSION <= PIMP_VERSION:
1175n/a import warnings
1176n/a warnings.warn("pimp_update is version %s, not newer than pimp version %s" %
1177n/a (pimp_update.PIMP_VERSION, PIMP_VERSION))
1178n/a else:
1179n/a from pimp_update import *
1180n/a
1181n/aif __name__ == '__main__':
1182n/a main()