summaryrefslogtreecommitdiff
path: root/pym
diff options
context:
space:
mode:
Diffstat (limited to 'pym')
-rw-r--r--pym/gentoolkit/__init__.py52
-rw-r--r--pym/gentoolkit/equery/__init__.py407
-rw-r--r--pym/gentoolkit/equery/belongs.py160
-rw-r--r--pym/gentoolkit/equery/changes.py336
-rw-r--r--pym/gentoolkit/equery/check.py232
-rw-r--r--pym/gentoolkit/equery/depends.py248
-rw-r--r--pym/gentoolkit/equery/depgraph.py194
-rw-r--r--pym/gentoolkit/equery/files.py311
-rw-r--r--pym/gentoolkit/equery/hasuse.py189
-rw-r--r--pym/gentoolkit/equery/list_.py251
-rw-r--r--pym/gentoolkit/equery/meta.py533
-rw-r--r--pym/gentoolkit/equery/size.py199
-rw-r--r--pym/gentoolkit/equery/uses.py340
-rw-r--r--pym/gentoolkit/equery/which.py98
-rw-r--r--pym/gentoolkit/errors.py92
-rw-r--r--pym/gentoolkit/glsa/__init__.py644
-rw-r--r--pym/gentoolkit/helpers.py162
-rw-r--r--pym/gentoolkit/helpers2.py425
-rw-r--r--pym/gentoolkit/package.py582
-rw-r--r--pym/gentoolkit/pprinter.py116
-rw-r--r--pym/gentoolkit/tests/equery/test_init.py43
-rw-r--r--pym/gentoolkit/tests/test_helpers2.py39
-rw-r--r--pym/gentoolkit/tests/test_template.py38
-rw-r--r--pym/gentoolkit/textwrap_.py97
24 files changed, 5788 insertions, 0 deletions
diff --git a/pym/gentoolkit/__init__.py b/pym/gentoolkit/__init__.py
new file mode 100644
index 0000000..62e359b
--- /dev/null
+++ b/pym/gentoolkit/__init__.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+#
+# Copyright 2003-2004 Karl Trygve Kalleberg
+# Copyright 2003-2009 Gentoo Technologies, Inc.
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
+
+__author__ = "Karl Trygve Kalleberg"
+__productname__ = "gentoolkit"
+__description__ = "Gentoolkit Common Library"
+
+import os
+import sys
+try:
+ import portage
+except ImportError:
+ sys.path.insert(0, "/usr/lib/portage/pym")
+ import portage
+import re
+try:
+ from threading import Lock
+except ImportError:
+ # If we don't have thread support, we don't need to worry about
+ # locking the global settings object. So we define a "null" Lock.
+ class Lock:
+ def acquire(self):
+ pass
+ def release(self):
+ pass
+
+try:
+ import portage.exception as portage_exception
+except ImportError:
+ import portage_exception
+
+try:
+ settingslock = Lock()
+ settings = portage.config(clone=portage.settings)
+ porttree = portage.db[portage.root]["porttree"]
+ vartree = portage.db[portage.root]["vartree"]
+ virtuals = portage.db[portage.root]["virtuals"]
+except portage_exception.PermissionDenied, e:
+ sys.stderr.write("Permission denied: '%s'\n" % str(e))
+ sys.exit(e.errno)
+
+Config = {
+ "verbosityLevel": 3
+}
+
+from helpers import *
+from package import *
diff --git a/pym/gentoolkit/equery/__init__.py b/pym/gentoolkit/equery/__init__.py
new file mode 100644
index 0000000..6bb04a9
--- /dev/null
+++ b/pym/gentoolkit/equery/__init__.py
@@ -0,0 +1,407 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Gentoo package query tool"""
+
+# Move to Imports section after Python 2.6 is stable
+from __future__ import with_statement
+
+__all__ = (
+ 'format_options',
+ 'format_package_names',
+ 'mod_usage'
+)
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import errno
+import sys
+import time
+from getopt import getopt, GetoptError
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit import catpkgsplit, settings, Package, Config
+from gentoolkit.textwrap_ import TextWrapper
+
+__productname__ = "equery"
+__authors__ = """\
+Karl Trygve Kalleberg - Original author
+Douglas Anderson - Modular redesign; author of meta, changes"""
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @param with_description (bool): Option to print module's __doc__ or not
+ """
+
+ if with_description:
+ print __doc__
+ print main_usage()
+ print
+ print pp.globaloption("global options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -q, --quiet", "minimal output"),
+ (" -C, --no-color", "turn off colors"),
+ (" -N, --no-pipe", "turn off pipe detection"),
+ (" -V, --version", "display version info")
+ ))
+ print
+ print pp.command("modules") + " (" + pp.command("short name") + ")"
+ print format_options((
+ (" (b)elongs", "list what package FILES belong to"),
+ (" (c)hanges", "list changelog entries for PKG"),
+ (" chec(k)", "verify checksums and timestamps for PKG"),
+ (" (d)epends", "list all packages directly depending on PKG"),
+ (" dep(g)raph", "display a tree of all dependencies for PKG"),
+ (" (f)iles", "list all files installed by PKG"),
+ (" (h)asuse", "list all packages that have USE flag"),
+ (" (l)ist", "list package matching PKG"),
+ (" (m)eta", "display metadata about PKG"),
+ (" (s)ize", "display total size of all files owned by PKG"),
+ (" (u)ses", "display USE flags for PKG"),
+ (" (w)hich", "print full path to ebuild for PKG")
+ ))
+
+
+def expand_module_name(module_name):
+ """Returns one of the values of name_map or raises KeyError"""
+
+ name_map = {
+ 'b': 'belongs',
+ 'c': 'changes',
+ 'k': 'check',
+ 'd': 'depends',
+ 'g': 'depgraph',
+ 'f': 'files',
+ 'h': 'hasuse',
+ 'l': 'list_',
+ 'm': 'meta',
+ 's': 'size',
+ 'u': 'uses',
+ 'w': 'which'
+ }
+
+ if module_name == 'list':
+ # list is a Python builtin type, so we must rename our module
+ return 'list_'
+ elif module_name in name_map.values():
+ return module_name
+ else:
+ return name_map[module_name]
+
+
+def format_options(options):
+ """Format module options.
+
+ @type options: list
+ @param options: [('option 1', 'description 1'), ('option 2', 'des... )]
+ @rtype: str
+ @return: formatted options string
+ """
+
+ result = []
+ twrap = TextWrapper(width=Config['termWidth'])
+ opts = (x[0] for x in options)
+ descs = (x[1] for x in options)
+ for opt, desc in zip(opts, descs):
+ twrap.initial_indent = pp.emph(opt.ljust(25))
+ twrap.subsequent_indent = " " * 25
+ result.append(twrap.fill(desc))
+
+ return '\n'.join(result)
+
+
+def format_package_names(match_set, status):
+ """Add location and mask status to package names.
+
+ @type match_set: list of gentoolkit.package.Package
+ @param match_set: packages to format
+ @rtype: list
+ @return: formatted packages
+ """
+
+ arch = gentoolkit.settings["ARCH"]
+ formatted_packages = []
+ pfxmodes = ['---', 'I--', '-P-', '--O']
+ maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-']
+
+ for pkg in match_set:
+ mask = get_mask_status(pkg, arch)
+ pkgcpv = pkg.get_cpv()
+ slot = pkg.get_env_var("SLOT")
+
+ formatted_packages.append("[%s] [%s] %s (%s)" %
+ (pfxmodes[status],
+ pp.maskflag(maskmodes[mask]),
+ pp.cpv(pkgcpv),
+ str(slot)))
+
+ return formatted_packages
+
+
+def format_filetype(path, fdesc, show_type=False, show_md5=False,
+ show_timestamp=False):
+ """Format a path for printing.
+
+ @type path: str
+ @param path: the path
+ @type fdesc: list
+ @param fdesc: [file_type, timestamp, MD5 sum/symlink target]
+ file_type is one of dev, dir, obj, sym.
+ If file_type is dir, there is no timestamp or MD5 sum.
+ If file_type is sym, fdesc[2] is the target of the symlink.
+ @type show_type: bool
+ @param show_type: if True, prepend the file's type to the formatted string
+ @type show_md5: bool
+ @param show_md5: if True, append MD5 sum to the formatted string
+ @type show_timestamp: bool
+ @param show_timestamp: if True, append time-of-creation after pathname
+ @rtype: str
+ @return: formatted pathname with optional added information
+ """
+
+ ftype = fpath = stamp = md5sum = ""
+
+ if fdesc[0] == "obj":
+ ftype = "file"
+ fpath = path
+ stamp = format_timestamp(fdesc[1])
+ md5sum = fdesc[2]
+ elif fdesc[0] == "dir":
+ ftype = "dir"
+ fpath = pp.path(path)
+ elif fdesc[0] == "sym":
+ ftype = "sym"
+ stamp = format_timestamp(fdesc[1])
+ tgt = fdesc[2].split()[0]
+ if Config["piping"]:
+ fpath = path
+ else:
+ fpath = pp.path_symlink(path + " -> " + tgt)
+ elif fdesc[0] == "dev":
+ ftype = "dev"
+ fpath = path
+ else:
+ pp.print_error("%s has unknown type: %s" % (path, fdesc[0]))
+
+ result = ""
+ if show_type:
+ result += "%4s " % ftype
+ result += fpath
+ if show_timestamp:
+ result += " " + stamp
+ if show_md5:
+ result += " " + md5sum
+
+ return result
+
+
+def format_timestamp(timestamp):
+ """Format a timestamp into, e.g., '2009-01-31 21:19:44' format"""
+
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(timestamp)))
+
+
+def get_mask_status(pkg, arch):
+ """Get the mask status of a given package.
+
+ @type pkg: gentoolkit.package.Package
+ @param pkg: pkg to get mask status of
+ @type arch: str
+ @param arch: output of gentoolkit.settings["ARCH"]
+ @rtype: int
+ @return: an index for this list: [" ", " ~", " -", "M ", "M~", "M-"]
+ 0 = not masked
+ 1 = keyword masked
+ 2 = arch masked
+ 3 = hard masked
+ 4 = hard and keyword masked,
+ 5 = hard and arch masked
+ """
+
+ # Determining mask status
+ keywords = pkg.get_env_var("KEYWORDS").split()
+ mask_status = 0
+ if pkg.is_masked():
+ mask_status += 3
+ if ("~%s" % arch) in keywords:
+ mask_status += 1
+ elif ("-%s" % arch) in keywords or "-*" in keywords:
+ mask_status += 2
+
+ return mask_status
+
+
+def initialize_configuration():
+ """Setup the standard equery config"""
+
+ # Get terminal size
+ term_width = pp.output.get_term_size()[1]
+ if term_width == -1:
+ # get_term_size() failed. Set a sane default width:
+ term_width = 80
+
+ # Terminal size, minus a 1-char margin for text wrapping
+ Config['termWidth'] = term_width - 1
+
+ # Color handling: -1: Use Portage settings, 0: Force off, 1: Force on
+ Config['color'] = -1
+
+ # Guess color output
+ if (Config['color'] == -1 and (not sys.stdout.isatty() or
+ settings["NOCOLOR"] in ("yes", "true")) or
+ Config['color'] == 0):
+ pp.output.nocolor()
+
+ # Guess piping output
+ if not sys.stdout.isatty():
+ Config["piping"] = True
+ else:
+ Config["piping"] = False
+
+
+def main_usage():
+ """Print the main usage message for equery"""
+
+ return "%(usage)s %(product)s [%(g_opts)s] %(mod_name)s [%(mod_opts)s]" % {
+ 'usage': pp.emph("Usage:"),
+ 'product': pp.productname(__productname__),
+ 'g_opts': pp.globaloption("global-options"),
+ 'mod_name': pp.command("module-name"),
+ 'mod_opts': pp.localoption("module-options")
+ }
+
+
+def mod_usage(mod_name="module", arg="pkgspec", optional=False):
+ """Provide a consistant usage message to the calling module.
+
+ @type arg: string
+ @param arg: what kind of argument the module takes (pkgspec, filename, etc)
+ @type optional: bool
+ @param optional: is the argument optional?
+ """
+
+ return "%(usage)s: %(mod_name)s [%(opts)s] %(arg)s" % {
+ 'usage': pp.emph("Usage"),
+ 'mod_name': pp.command(mod_name),
+ 'opts': pp.localoption("options"),
+ 'arg': ("[%s]" % pp.emph(arg)) if optional else pp.emph(arg)
+ }
+
+
+def parse_global_options(global_opts, args):
+ """Parse global input args and return True if we should display help for
+ the called module, else False (or display help and exit from here).
+ """
+
+ need_help = False
+ opts = (opt[0] for opt in global_opts)
+ for opt in opts:
+ if opt in ('-h', '--help'):
+ if args:
+ need_help = True
+ else:
+ print_help()
+ sys.exit(0)
+ elif opt in ('-q','--quiet'):
+ Config["verbosityLevel"] = 0
+ elif opt in ('-C', '--no-color', '--nocolor'):
+ Config['color'] = 0
+ pp.output.nocolor()
+ elif opt in ('-N', '--no-pipe'):
+ Config["piping"] = False
+ elif opt in ('-V', '--version'):
+ print_version()
+ sys.exit(0)
+
+ return need_help
+
+
+def print_version():
+ """Print the version of this tool to the console."""
+
+ try:
+ with open('/etc/gentoolkit-version') as gentoolkit_version:
+ version = gentoolkit_version.read().strip()
+ except IOError, err:
+ pp.die(2, str(err))
+
+ print "%(product)s (%(version)s) - %(docstring)s" % {
+ "product": pp.productname(__productname__),
+ "version": version,
+ "docstring": __doc__
+ }
+ print
+ print __authors__
+
+
+def split_arguments(args):
+ """Separate module name from module arguments"""
+
+ return args.pop(0), args
+
+
+def main():
+ """Parse input and run the program."""
+
+ initialize_configuration()
+
+ short_opts = "hqCNV"
+ long_opts = ('help', 'quiet', 'nocolor', 'no-color', 'no-pipe', 'version')
+
+ try:
+ global_opts, args = getopt(sys.argv[1:], short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Global %s" % err)
+ print_help(with_description=False)
+ sys.exit(2)
+
+
+ # Parse global options
+ need_help = parse_global_options(global_opts, args)
+
+ try:
+ module_name, module_args = split_arguments(args)
+ except IndexError:
+ print_help()
+ sys.exit(2)
+
+ if need_help:
+ module_args.append('--help')
+
+ try:
+ expanded_module_name = expand_module_name(module_name)
+ except KeyError:
+ pp.print_error("Unknown module '%s'" % module_name)
+ print_help(with_description=False)
+ sys.exit(2)
+
+ try:
+ loaded_module = __import__(expanded_module_name, globals(),
+ locals(), [], -1)
+ loaded_module.main(module_args)
+ except ValueError, err:
+ if isinstance(err[0], list):
+ pp.print_error("Ambiguous package name. Use one of: ")
+ while err[0]:
+ print " " + err[0].pop()
+ else:
+ pp.print_error("Internal portage error, terminating")
+ if err:
+ pp.print_error(str(err[0]))
+ sys.exit(1)
+ except IOError, err:
+ if err.errno != errno.EPIPE:
+ raise
diff --git a/pym/gentoolkit/equery/belongs.py b/pym/gentoolkit/equery/belongs.py
new file mode 100644
index 0000000..6408ec7
--- /dev/null
+++ b/pym/gentoolkit/equery/belongs.py
@@ -0,0 +1,160 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""List all packages owning a particular file
+
+Note: Normally, only one package will own a file. If multiple packages own
+ the same file, it usually consitutes a problem, and should be reported.
+"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import re
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_filetype, format_options, mod_usage, \
+ Config
+from gentoolkit.helpers2 import get_installed_cpvs
+from gentoolkit.package import Package
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "fullRegex": False,
+ "earlyOut": False,
+ "nameOnly": False
+}
+
+# =========
+# Functions
+# =========
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ for opt in opts:
+ if opt in ('-h','--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-c', '--category'):
+ # Remove this warning after a reasonable amount of time
+ # (djanderson, 2/2009)
+ pp.print_warn("Module option -c, --category not implemented")
+ print
+ elif opt in ('-e', '--early-out', '--earlyout'):
+ if opt == '--earlyout':
+ pp.print_warn("Use of --earlyout is deprecated.")
+ pp.print_warn("Please use --early-out.")
+ print
+ QUERY_OPTS['earlyOut'] = True
+ elif opt in ('-f', '--full-regex'):
+ QUERY_OPTS['fullRegex'] = True
+ elif opt in ('-n', '--name-only'):
+ QUERY_OPTS['nameOnly'] = True
+
+
+def prepare_search_regex(queries):
+ """Create a regex out of the queries"""
+
+ if QUERY_OPTS["fullRegex"]:
+ result = queries
+ else:
+ result = []
+ # Trim trailing and multiple slashes from queries
+ slashes = re.compile('/+')
+ for query in queries:
+ query = slashes.sub('/', query).rstrip('/')
+ if query.startswith('/'):
+ query = "^%s$" % re.escape(query)
+ else:
+ query = "/%s$" % re.escape(query)
+ result.append(query)
+
+ result = "|".join(result)
+
+ return re.compile(result)
+
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="belongs", arg="filename")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -f, --full-regex", "supplied query is a regex" ),
+ (" -e, --early-out", "stop when first match is found"),
+ (" -n, --name-only", "don't print the version")
+ ))
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ # -c, --category is not implemented
+ short_opts = "hc:fen"
+ long_opts = ('help', 'category=', 'full-regex', 'early-out', 'earlyout',
+ 'name-only')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ query_re = prepare_search_regex(queries)
+
+ if not Config["piping"]:
+ pp.print_info(3, " * Searching for %s ... "
+ % (pp.regexpquery(",".join(queries))))
+
+ matches = get_installed_cpvs()
+
+ # Print matches to screen or pipe
+ found_match = False
+ for pkg in [Package(x) for x in matches]:
+ files = pkg.get_contents()
+ for cfile in files:
+ if query_re.search(cfile):
+ if QUERY_OPTS["nameOnly"]:
+ pkg_str = pkg.key
+ else:
+ pkg_str = pkg.cpv
+ if Config['piping']:
+ print pkg_str
+ else:
+ file_str = pp.path(format_filetype(cfile, files[cfile]))
+ pp.print_info(0, "%s (%s)" % (pkg_str, file_str))
+
+ found_match = True
+
+ if found_match and QUERY_OPTS["earlyOut"]:
+ break
diff --git a/pym/gentoolkit/equery/changes.py b/pym/gentoolkit/equery/changes.py
new file mode 100644
index 0000000..b7644be
--- /dev/null
+++ b/pym/gentoolkit/equery/changes.py
@@ -0,0 +1,336 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""Display the Gentoo ChangeLog entry for the latest installable version of a
+given package
+"""
+
+# Move to Imports sections when Python 2.6 is stable
+from __future__ import with_statement
+
+__author__ = 'Douglas Anderson'
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from getopt import gnu_getopt, GetoptError
+
+from portage.versions import pkgsplit
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage
+from gentoolkit.helpers2 import find_best_match, find_packages
+from gentoolkit.package import Package, VersionMatch
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ 'onlyLatest': False,
+ 'showFullLog': False,
+ 'limit': None,
+ 'from': None,
+ 'to': None
+}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="changes")
+ print
+ print pp.emph("examples")
+ print (" c portage # show latest visible "
+ "version's entry")
+ print " c portage --full --limit=3 # show 3 latest entries"
+ print " c '=sys-apps/portage-2.1.6*' # use atom syntax"
+ print " c portage --from=2.2_rc20 --to=2.2_rc30 # use version ranges"
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -l, --latest", "display only the latest ChangeLog entry"),
+ (" -f, --full", "display the full ChangeLog"),
+ (" --limit=NUM",
+ "limit the number of entries displayed (with --full)"),
+ (" --from=VER", "set which version to display from"),
+ (" --to=VER", "set which version to display to"),
+ ))
+
+
+def get_logpath(pkg):
+ """Test that the package's ChangeLog path is valid and readable, else
+ die.
+
+ @type pkg: gentoolkit.package.Package
+ @param pkg: package to find logpath for
+ @rtype: str
+ @return: a path to a readable ChangeLog
+ """
+
+ logpath = os.path.join(pkg.get_package_path(), 'ChangeLog')
+ if not os.path.isfile(logpath) or not os.access(logpath, os.R_OK):
+ pp.die(1, "%s does not exist or is unreadable"
+ % pp.path(logpath))
+
+ return logpath
+
+
+def get_match(query):
+ """Find a valid package to get the ChangeLog path from or raise
+ GentoolkitNoMatches.
+ """
+
+ match = matches = None
+ match = find_best_match(query)
+
+ if not match:
+ matches = find_packages(query, include_masked=True)
+ else:
+ matches = [match]
+
+ if not matches:
+ pp.print_warn("Try using an unversioned query with "
+ "--from and --to.")
+ raise errors.GentoolkitNoMatches(query)
+
+ return matches[0]
+
+
+def index_changelog(entries):
+ """Convert the list from split_changelog into a dict with VersionMatch
+ instance as the index.
+
+ @todo: UPDATE THIS
+ @type entries: list
+ @param entries: output of split_changelog
+ @rtype: dict
+ @return: dict with gentoolkit.package.Package instances as keys and the
+ corresponding ChangeLog entree as its value
+ """
+
+ result = []
+ for entry in entries:
+ # Extract the package name from the entry, ex:
+ # *xterm-242 (07 Mar 2009) => xterm-242
+ pkg_name = entry.split(' ', 1)[0].lstrip('*')
+ pkg_split = pkgsplit(pkg_name)
+ result.append(
+ (VersionMatch(op="=", ver=pkg_split[1], rev=pkg_split[2]), entry))
+
+ return result
+
+
+def is_ranged(atom):
+ """Return True if an atom string appears to be ranged, else False."""
+
+ return atom.startswith(('~', '<', '>')) or atom.endswith('*')
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-f', '--full'):
+ QUERY_OPTS['showFullLog'] = True
+ elif opt in ('-l', '--latest'):
+ QUERY_OPTS['onlyLatest'] = True
+ elif opt in ('--limit',):
+ set_limit(posarg)
+ elif opt in ('--from',):
+ set_from(posarg)
+ elif opt in ('--to',):
+ set_to(posarg)
+
+
+def print_matching_entries(indexed_entries, pkg, first_run):
+ """Print only the entries which interect with the pkg version."""
+
+ from_restriction = QUERY_OPTS['from']
+ to_restriction = QUERY_OPTS['to']
+
+ for entry_set in indexed_entries:
+ i, entry = entry_set
+ # a little hackery, since versionmatch doesn't store the
+ # package key, but intersects checks that it matches.
+ i.key = pkg.key
+ if from_restriction or to_restriction:
+ if from_restriction and not from_restriction.match(i):
+ continue
+ if to_restriction and not to_restriction.match(i):
+ continue
+ elif not pkg.intersects(i):
+ continue
+
+ if not first_run:
+ print "\n"
+ print entry.strip()
+ first_run = False
+
+ return first_run
+
+
+def set_from(posarg):
+ """Set a starting version to filter the ChangeLog with or die if posarg
+ is not a valid version.
+ """
+
+ pkg_split = pkgsplit('null-%s' % posarg)
+
+ if pkg_split and not is_ranged(posarg):
+ ver_match = VersionMatch(
+ op=">=",
+ ver=pkg_split[1],
+ rev=pkg_split[2] if pkg_split[2] != 'r0' else '')
+ QUERY_OPTS['from'] = ver_match
+ else:
+ err = "Module option --from requires valid unranged version (got '%s')"
+ pp.print_error(err % posarg)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+
+def set_limit(posarg):
+ """Set a limit in QUERY_OPTS on how many ChangeLog entries to display or
+ die if posarg is not an integer.
+ """
+
+ if posarg.isdigit():
+ QUERY_OPTS['limit'] = int(posarg)
+ else:
+ err = "Module option --limit requires integer (got '%s')"
+ pp.print_error(err % posarg)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+
+def set_to(posarg):
+ """Set an ending version to filter the ChangeLog with or die if posarg
+ is not a valid version.
+ """
+
+ pkg_split = pkgsplit('null-%s' % posarg)
+ if pkg_split and not is_ranged(posarg):
+ ver_match = VersionMatch(
+ op="<=",
+ ver=pkg_split[1],
+ rev=pkg_split[2] if pkg_split[2] != 'r0' else '')
+ QUERY_OPTS['to'] = ver_match
+ else:
+ err = "Module option --to requires valid unranged version (got '%s')"
+ pp.print_error(err % posarg)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+
+def split_changelog(logpath):
+ """Split the changelog up into individual entries.
+
+ @type logpath: str
+ @param logpath: valid path to ChangeLog file
+ @rtype: list
+ @return: individual ChangeLog entrees
+ """
+
+ result = []
+ partial_entries = []
+ with open(logpath) as log:
+ for line in log:
+ if line.startswith('#'):
+ continue
+ elif line.startswith('*'):
+ # Append last entry to result...
+ entry = ''.join(partial_entries)
+ if entry and not entry.isspace():
+ result.append(entry)
+ # ... and start a new entry
+ partial_entries = [line]
+ else:
+ partial_entries.append(line)
+ else:
+ # Append the final entry
+ entry = ''.join(partial_entries)
+ result.append(entry)
+
+ return result
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hlf"
+ long_opts = ('help', 'full', 'from=', 'latest', 'limit=', 'to=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ ranged_query = None
+ if is_ranged(query):
+ # Raises GentoolkitInvalidCPV here if invalid
+ ranged_query = Package(query)
+
+ pkg = get_match(query)
+ logpath = get_logpath(pkg)
+ log_entries = split_changelog(logpath)
+ indexed_entries = index_changelog(log_entries)
+
+ #
+ # Output
+ #
+
+ if QUERY_OPTS['onlyLatest']:
+ print log_entries[0].strip()
+ elif QUERY_OPTS['showFullLog']:
+ end = QUERY_OPTS['limit'] or len(log_entries)
+ for entry in log_entries[:end]:
+ print entry
+ first_run = False
+ else:
+ if ranged_query:
+ pkg = ranged_query
+ first_run = print_matching_entries(indexed_entries, pkg, first_run)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/check.py b/pym/gentoolkit/equery/check.py
new file mode 100644
index 0000000..ffddf72
--- /dev/null
+++ b/pym/gentoolkit/equery/check.py
@@ -0,0 +1,232 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Check timestamps and MD5sums for files owned by a given installed package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from getopt import gnu_getopt, GetoptError
+
+try:
+ import portage.checksum as checksum
+except ImportError:
+ import portage_checksum as checksum
+
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_options, mod_usage, Config
+from gentoolkit.helpers2 import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": False,
+ "includeOverlayTree": False,
+ "includePortTree": False,
+ "checkMD5sum": True,
+ "checkTimestamp" : True,
+ "isRegex": False,
+ "matchExact": True,
+ "printMatchInfo": False,
+ "showSummary" : True,
+ "showPassedFiles" : False,
+ "showFailedFiles" : True
+}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+
+ # Deprecation warning added by djanderson, 12/2008
+ pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.")
+ pp.print_warn("Use globbing to simulate the old behavior (see man equery).")
+ pp.print_warn("Use '*' to check all installed packages.")
+ print
+
+ print mod_usage(mod_name="check")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -c, --category CAT", "only check files from packages in CAT"),
+ (" -f, --full-regex", "query is a regular expression"),
+ ))
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-c', '--category'):
+ QUERY_OPTS['categoryFilter'] = posarg
+ elif opt in ('-f', '--full-regex'):
+ QUERY_OPTS['isRegex'] = True
+
+
+def run_checks(files):
+ """Run some basic sanity checks on a package's contents.
+
+ If the file type (ftype) is not a directory or symlink, optionally
+ verify MD5 sums or mtimes via verify_obj().
+
+ @see: gentoolkit.packages.get_contents()
+ @type files: dict
+ @param files: in form {'PATH': ['TYPE', 'TIMESTAMP', 'MD5SUM']}
+ @rtype: tuple
+ @return:
+ passed (int): number of files that passed all checks
+ checked (int): number of files checked
+ errs (list): check errors' descriptions
+ """
+
+ checked = 0
+ passed = 0
+ errs = []
+ for cfile in files:
+ checked += 1
+ ftype = files[cfile][0]
+ if not os.path.exists(cfile):
+ errs.append("%s does not exist" % cfile)
+ continue
+ elif ftype == "dir":
+ if not os.path.isdir(cfile):
+ err = "%(cfile)s exists, but is not a directory"
+ errs.append(err % locals())
+ continue
+ elif ftype == "obj":
+ new_errs = verify_obj(files, cfile, errs)
+ if new_errs != errs:
+ errs = new_errs
+ continue
+ elif ftype == "sym":
+ target = files[cfile][2].strip()
+ if not os.path.islink(cfile):
+ err = "%(cfile)s exists, but is not a symlink"
+ errs.append(err % locals())
+ continue
+ tgt = os.readlink(cfile)
+ if tgt != target:
+ err = "%(cfile)s does not point to %(target)s"
+ errs.append(err % locals())
+ continue
+ else:
+ err = "%(cfile)s has unknown type %(ftype)s"
+ errs.append(err % locals())
+ continue
+ passed += 1
+
+ return passed, checked, errs
+
+
+def verify_obj(files, cfile, errs):
+ """Verify the MD5 sum and/or mtime and return any errors."""
+
+ if QUERY_OPTS["checkMD5sum"]:
+ md5sum = files[cfile][2]
+ try:
+ cur_checksum = checksum.perform_md5(cfile, calc_prelink=1)
+ except IOError:
+ err = "Insufficient permissions to read %(cfile)s"
+ errs.append(err % locals())
+ return errs
+ if cur_checksum != md5sum:
+ err = "%(cfile)s has incorrect MD5sum"
+ errs.append(err % locals())
+ return errs
+ if QUERY_OPTS["checkTimestamp"]:
+ mtime = int(files[cfile][1])
+ st_mtime = os.lstat(cfile).st_mtime
+ if st_mtime != mtime:
+ err = "%(cfile)s has wrong mtime (is %(st_mtime)d, " + \
+ "should be %(mtime)d)"
+ errs.append(err % locals())
+ return errs
+
+ return errs
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hac:f"
+ long_opts = ('help', 'all', 'category=', 'full-regex')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries and not QUERY_OPTS["includeInstalled"]:
+ print_help()
+ sys.exit(2)
+ elif queries and not QUERY_OPTS["includeInstalled"]:
+ QUERY_OPTS["includeInstalled"] = True
+ elif QUERY_OPTS["includeInstalled"]:
+ queries = ["*"]
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ pp.print_error("No package found matching %s" % query)
+
+ matches.sort()
+
+ for pkg in matches:
+ if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ print "[ Checking %s ]" % pp.cpv(pkg.cpv)
+ else:
+ print "%s:" % pkg.cpv
+
+ passed, checked, errs = run_checks(pkg.get_contents())
+
+ if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ for err in errs:
+ pp.print_error(err)
+
+ passed = pp.number(str(passed))
+ checked = pp.number(str(checked))
+ info = " * %(passed)s out of %(checked)s files passed"
+ print info % locals()
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/depends.py b/pym/gentoolkit/equery/depends.py
new file mode 100644
index 0000000..394c35b
--- /dev/null
+++ b/pym/gentoolkit/equery/depends.py
@@ -0,0 +1,248 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""List all direct dependencies matching a given query"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+from portage.util import unique_array
+
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_options, mod_usage, Config
+from gentoolkit.helpers2 import compare_package_strings, do_lookup, \
+ find_packages, get_cpvs, get_installed_cpvs
+from gentoolkit.package import Package
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": True,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "isRegex": False,
+ "matchExact": True,
+ "onlyDirect": True,
+ "onlyInstalled": True,
+ "printMatchInfo": True,
+ "indentLevel": 0,
+ "depth": -1
+}
+
+# Used to cache and detect looping
+PKGSEEN = set()
+PKGDEPS = {}
+DEPPKGS = {}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="depends")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -a, --all-packages",
+ "include packages that are not installed (slow)"),
+ (" -D, --indirect",
+ "search both direct and indirect dependencies"),
+ (" --depth=N", "limit indirect dependency tree to specified depth")
+ ))
+
+
+def cache_package_list(pkg_cache=None):
+ """Ensure that the package cache is set."""
+
+ if not pkg_cache:
+ if QUERY_OPTS["onlyInstalled"]:
+ # TODO: move away from using strings here
+ packages = get_installed_cpvs()
+ else:
+ packages = get_cpvs()
+ packages.sort(compare_package_strings)
+ pkg_cache = packages
+ else:
+ packages = pkg_cache
+
+ return packages
+
+
+def display_dependencies(cpv_is_displayed, dependency, cpv):
+ """Output dependencies calculated by find_dependencies.
+
+ @type cpv_is_displayed: bool
+ @param cpv_is_displayed: if True, the cpv has already been printed
+ @see: gentoolkit.package.get_*_deps()
+ @type dependency: tuple
+ @param dependency: (comparator, [use flags], cpv)
+ @type cpv: string
+ @param cpv: cat/pkg-ver
+ """
+
+ atom = pp.pkgquery(dependency[0] + dependency[2])
+ indent = " " * (QUERY_OPTS["indentLevel"] * 2)
+ useflags = pp.useflag(" & ".join(dependency[1]))
+
+ if not cpv_is_displayed:
+ if dependency[1]:
+ if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ print indent + pp.cpv(cpv),
+ print "(" + useflags + " ? " + atom + ")"
+ else:
+ print indent + cpv
+ else:
+ if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ print indent + pp.cpv(cpv),
+ print "(" + atom + ")"
+ else:
+ print indent + cpv
+ elif not Config["piping"] and Config["verbosityLevel"] >= 3:
+ indent = indent + " " * len(cpv)
+ if dependency[1]:
+ print indent + " (" + useflags + " ? " + atom + ")"
+ else:
+ print indent + " (" + atom + ")"
+
+
+def find_dependencies(matches, pkg_cache):
+ """Find dependencies for the packaged named in queries.
+
+ @type queries: list
+ @param queries: packages to find the dependencies for
+ """
+
+ for pkg in [Package(x) for x in cache_package_list(pkg_cache)]:
+ if not pkg.cpv in PKGDEPS:
+ try:
+ deps = pkg.get_runtime_deps() + pkg.get_compiletime_deps()
+ deps.extend(pkg.get_postmerge_deps())
+ except KeyError:
+ # If the ebuild is not found...
+ continue
+ # Remove duplicate deps
+ deps = unique_array(deps)
+ PKGDEPS[pkg.cpv] = deps
+ else:
+ deps = PKGDEPS[pkg.cpv]
+
+ cpv_is_displayed = False
+ for dependency in deps:
+ # TODO: (old) determine if dependency is enabled by USE flag
+ # Find all packages matching the dependency
+ depstr = dependency[0] + dependency[2]
+ if not depstr in DEPPKGS:
+ depcpvs = find_packages(depstr)
+ DEPPKGS[depstr] = depcpvs
+ else:
+ depcpvs = DEPPKGS[depstr]
+
+ for depcpv in depcpvs:
+ is_match = False
+ if depcpv in matches:
+ is_match = True
+
+ if is_match:
+ display_dependencies(cpv_is_displayed, dependency, pkg.cpv)
+ cpv_is_displayed = True
+ break
+
+ # if --indirect specified, call ourselves again with the dependency
+ # Do not call if we have already called ourselves.
+ if (cpv_is_displayed and not QUERY_OPTS["onlyDirect"] and
+ pkg not in PKGSEEN and
+ (QUERY_OPTS["indentLevel"] < QUERY_OPTS["depth"] or
+ QUERY_OPTS["depth"] == -1)):
+
+ PKGSEEN.add(pkg)
+ QUERY_OPTS["indentLevel"] += 1
+ find_dependencies([pkg], pkg_cache)
+ QUERY_OPTS["indentLevel"] -= 1
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-a', '--all-packages'):
+ QUERY_OPTS["onlyInstalled"] = False
+ elif opt in ('-d', '--direct'):
+ continue
+ elif opt in ('-D', '--indirect'):
+ QUERY_OPTS["onlyDirect"] = False
+ elif opt in ('--depth'):
+ if posarg.isdigit():
+ depth = int(posarg)
+ else:
+ err = "Module option --depth requires integer (got '%s')"
+ pp.print_error(err % posarg)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+ QUERY_OPTS["depth"] = depth
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hadD" # -d, --direct was old option for default action
+ long_opts = ('help', 'all-packages', 'direct', 'indirect', 'depth=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if matches:
+ find_dependencies(matches, None)
+ else:
+ pp.print_error("No matching package found for %s" % query)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/depgraph.py b/pym/gentoolkit/equery/depgraph.py
new file mode 100644
index 0000000..f4723c2
--- /dev/null
+++ b/pym/gentoolkit/equery/depgraph.py
@@ -0,0 +1,194 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Display a dependency graph for a given package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, Config
+from gentoolkit.helpers2 import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "depth": 0,
+ "displayUseflags": True,
+ "fancyFormat": True,
+ "includeInstalled": True,
+ "includePortTree": True,
+ "includeOverlayTree": True,
+ "includeMasked": True,
+ "isRegex": False,
+ "matchExact": True,
+ "printMatchInfo": True
+}
+
+if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ VERBOSE = True
+else:
+ VERBOSE = False
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="depgraph")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -U, --no-useflags", "do not show USE flags"),
+ (" -l, --linear", "do not use fancy formatting"),
+ (" --depth=N", "limit dependency graph to specified depth")
+ ))
+
+
+def display_graph(pkg, stats, level=0, seen_pkgs=None, suffix=""):
+ """Display a dependency graph for a package
+
+ @type pkg: gentoolkit.package.Package
+ @param pkg: package to check dependencies of
+ @type level: int
+ @param level: current depth level
+ @type seen_pkgs: set
+ @param seen_pkgs: a set of all packages that have had their deps graphed
+ """
+
+ if not seen_pkgs:
+ seen_pkgs = set()
+
+ stats["packages"] += 1
+ stats["maxdepth"] = max(stats["maxdepth"], level)
+
+ pfx = ""
+ if QUERY_OPTS["fancyFormat"]:
+ pfx = (level * " ") + "`-- "
+ pp.print_info(0, pfx + pkg.cpv + suffix)
+
+ seen_pkgs.add(pkg.cpv)
+
+ deps = pkg.get_runtime_deps() + pkg.get_compiletime_deps()
+ deps.extend(pkg.get_postmerge_deps())
+ for dep in deps:
+ suffix = ""
+ depcpv = dep[2]
+ deppkg = gentoolkit.find_best_match(dep[0] + depcpv)
+ if not deppkg:
+ print (pfx + dep[0] + depcpv),
+ print "(unable to resolve: package masked or removed)"
+ continue
+ if deppkg.get_cpv() in seen_pkgs:
+ continue
+ if depcpv.find("virtual") == 0:
+ suffix += " (%s)" % pp.cpv(depcpv)
+ if dep[1] and QUERY_OPTS["displayUseflags"]:
+ suffix += " [%s]" % pp.useflagon(' '.join(dep[1]))
+ if (level < QUERY_OPTS["depth"] or QUERY_OPTS["depth"] <= 0):
+ seen_pkgs, stats = display_graph(deppkg, stats, level+1,
+ seen_pkgs, suffix)
+
+ return seen_pkgs, stats
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ if opt in ('-U', '--no-useflags'):
+ QUERY_OPTS["displayUseflags"] = False
+ if opt in ('-l', '--linear'):
+ QUERY_OPTS["fancyFormat"] = False
+ if opt in ('--depth'):
+ if posarg.isdigit():
+ depth = int(posarg)
+ else:
+ err = "Module option --depth requires integer (got '%s')"
+ pp.print_error(err % posarg)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+ QUERY_OPTS["depth"] = depth
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hUl"
+ long_opts = ('help', 'no-useflags', 'depth=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ errors.GentoolkitNoMatches(query)
+
+ for pkg in matches:
+ stats = {"maxdepth": 0, "packages": 0}
+
+ if VERBOSE:
+ pp.print_info(3, " * dependency graph for %s:" % pp.cpv(pkg.cpv))
+ else:
+ pp.print_info(0, "%s:" % pkg.cpv)
+
+ stats = display_graph(pkg, stats)[1]
+
+ if VERBOSE:
+ info = ''.join(["[ ", pp.cpv(pkg.cpv), " stats: packages (",
+ pp.number(str(stats["packages"])), "), max depth (",
+ pp.number(str(stats["maxdepth"])), ") ]"])
+ pp.print_info(0, info)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/files.py b/pym/gentoolkit/equery/files.py
new file mode 100644
index 0000000..f25ae5a
--- /dev/null
+++ b/pym/gentoolkit/equery/files.py
@@ -0,0 +1,311 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""List files owned by a given package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_filetype, format_options, mod_usage, \
+ Config
+from gentoolkit.helpers2 import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": True,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False,
+ "matchExact": True,
+ "outputTree": False,
+ "printMatchInfo": True,
+ "showType": False,
+ "showTimestamp": False,
+ "showMD5": False,
+ "typeFilter": None
+}
+
+FILTER_RULES = ('dir', 'obj', 'sym', 'dev', 'path', 'conf', 'cmd', 'doc',
+ 'man', 'info')
+
+if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ VERBOSE = True
+else:
+ VERBOSE = False
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="files")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -m, --md5sum", "include MD5 sum in output"),
+ (" -s, --timestamp", "include timestamp in output"),
+ (" -t, --type", "include file type in output"),
+ (" --tree", "display results in a tree (turns off other options)"),
+ (" -f, --filter=RULES", "filter output by file type"),
+ (" RULES",
+ "a comma-separated list (no spaces); choose from:")
+ ))
+ print " " * 24, ', '.join(pp.emph(x) for x in FILTER_RULES)
+
+
+def display_files(contents):
+ """Display the content of an installed package.
+
+ @see: gentoolkit.package.Package.get_contents
+ @type contents: dict
+ @param contents: {'path': ['filetype', ...], ...}
+ """
+
+ filenames = contents.keys()
+ filenames.sort()
+ last = []
+
+ for name in filenames:
+ if QUERY_OPTS["outputTree"]:
+ basename = name.split("/")[1:]
+ if contents[name][0] == "dir":
+ if len(last) == 0:
+ last = basename
+ print pp.path(" /" + basename[0])
+ continue
+ numol = 0
+ for i, directory in enumerate(basename):
+ try:
+ if directory in last[i]:
+ numol = i + 1
+ continue
+ # W0704: Except doesn't do anything
+ # pylint: disable-msg=W0704
+ except IndexError:
+ pass
+ last = basename
+ if len(last) == 1:
+ print pp.path(" " + last[0])
+ continue
+ ind = " " * (numol * 3)
+ print pp.path(ind + "> " + "/" + last[-1])
+ elif contents[name][0] == "sym":
+ print pp.path(" " * (len(last) * 3) + "+"),
+ print pp.path_symlink(basename[-1] + " -> " + contents[name][2])
+ else:
+ print pp.path(" " * (len(last) * 3) + "+ ") + basename[-1]
+ else:
+ pp.print_info(0, format_filetype(
+ name,
+ contents[name],
+ show_type=QUERY_OPTS["showType"],
+ show_md5=QUERY_OPTS["showMD5"],
+ show_timestamp=QUERY_OPTS["showTimestamp"]))
+
+
+def filter_by_doc(contents, content_filter):
+ """Return a copy of content filtered by documentation."""
+
+ filtered_content = {}
+ for doctype in ('doc' ,'man' ,'info'):
+ # List only files from /usr/share/{doc,man,info}
+ if doctype in content_filter:
+ docpath = os.path.join(os.sep, 'usr', 'share', doctype)
+ for path in contents:
+ if contents[path][0] == 'obj' and path.startswith(docpath):
+ filtered_content[path] = contents[path]
+
+ return filtered_content
+
+
+def filter_by_command(contents):
+ """Return a copy of content filtered by executable commands."""
+
+ filtered_content = {}
+ userpath = os.environ["PATH"].split(os.pathsep)
+ userpath = [os.path.normpath(x) for x in userpath]
+ for path in contents:
+ if (contents[path][0] in ['obj', 'sym'] and
+ os.path.dirname(path) in userpath):
+ filtered_content[path] = contents[path]
+
+ return filtered_content
+
+
+def filter_by_path(contents):
+ """Return a copy of content filtered by file paths."""
+
+ filtered_content = {}
+ paths = list(reversed(sorted(contents.keys())))
+ while paths:
+ basepath = paths.pop()
+ if contents[basepath][0] == 'dir':
+ check_subdirs = False
+ for path in paths:
+ if (contents[path][0] != "dir" and
+ os.path.dirname(path) == basepath):
+ filtered_content[basepath] = contents[basepath]
+ check_subdirs = True
+ break
+ if check_subdirs:
+ while (paths and paths[-1].startswith(basepath)):
+ paths.pop()
+
+ return filtered_content
+
+
+def filter_by_conf(contents):
+ """Return a copy of content filtered by configuration files."""
+
+ filtered_content = {}
+ conf_path = gentoolkit.settings["CONFIG_PROTECT"].split()
+ conf_path = tuple(os.path.normpath(x) for x in conf_path)
+ conf_mask_path = gentoolkit.settings["CONFIG_PROTECT_MASK"].split()
+ conf_mask_path = tuple(os.path.normpath(x) for x in conf_mask_path)
+ for path in contents:
+ if contents[path][0] == 'obj' and path.startswith(conf_path):
+ if not path.startswith(conf_mask_path):
+ filtered_content[path] = contents[path]
+
+ return filtered_content
+
+
+def filter_contents(contents):
+ """Filter files by type if specified by the user.
+
+ @see: gentoolkit.package.Package.get_contents
+ @type contents: dict
+ @param contents: {'path': ['filetype', ...], ...}
+ @rtype: dict
+ @return: contents with unrequested filetypes stripped
+ """
+
+ if QUERY_OPTS['typeFilter']:
+ content_filter = QUERY_OPTS['typeFilter']
+ else:
+ return contents
+
+ filtered_content = {}
+ if frozenset(('dir', 'obj', 'sym', 'dev')).intersection(content_filter):
+ # Filter elements by type (as recorded in CONTENTS)
+ for path in contents:
+ if contents[path][0] in content_filter:
+ filtered_content[path] = contents[path]
+ if "cmd" in content_filter:
+ filtered_content.update(filter_by_command(contents))
+ if "path" in content_filter:
+ filtered_content.update(filter_by_path(contents))
+ if "conf" in content_filter:
+ filtered_content.update(filter_by_conf(contents))
+ if frozenset(('doc' ,'man' ,'info')).intersection(content_filter):
+ filtered_content.update(filter_by_doc(contents, content_filter))
+
+ return filtered_content
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ content_filter = []
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-e', '--exact-name'):
+ QUERY_OPTS["matchExact"] = True
+ elif opt in ('-m', '--md5sum'):
+ QUERY_OPTS["showMD5"] = True
+ elif opt in ('-s', '--timestamp'):
+ QUERY_OPTS["showTimestamp"] = True
+ elif opt in ('-t', '--type'):
+ QUERY_OPTS["showType"] = True
+ elif opt in ('--tree'):
+ QUERY_OPTS["outputTree"] = True
+ elif opt in ('-f', '--filter'):
+ f_split = posarg.split(',')
+ content_filter.extend(x.lstrip('=') for x in f_split)
+ for rule in content_filter:
+ if not rule in FILTER_RULES:
+ pp.print_error("Invalid filter rule '%s'" % rule)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+ QUERY_OPTS["typeFilter"] = content_filter
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hemstf:"
+ long_opts = ('help', 'exact-name', 'md5sum', 'timestamp', 'type', 'tree',
+ 'filter=')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ # Turn off filtering for tree output
+ if QUERY_OPTS["outputTree"]:
+ QUERY_OPTS["typeFilter"] = None
+
+ #
+ # Output files
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ pp.print_error("No matching packages found for %s" % query)
+
+ for pkg in matches:
+ if VERBOSE:
+ pp.print_info(1, " * Contents of %s:" % pp.cpv(pkg.cpv))
+
+ contents = pkg.get_contents()
+ display_files(filter_contents(contents))
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/hasuse.py b/pym/gentoolkit/equery/hasuse.py
new file mode 100644
index 0000000..6580bbf
--- /dev/null
+++ b/pym/gentoolkit/equery/hasuse.py
@@ -0,0 +1,189 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""List all installed packages that have a given USE flag"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_options, format_package_names, \
+ mod_usage, Config
+from gentoolkit.helpers2 import do_lookup, get_installed_cpvs
+from gentoolkit.package import Package
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": False,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False, # Necessary for do_lookup, don't change
+ "printMatchInfo": False
+}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="hasuse", arg="USE-flag")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -i, --installed",
+ "include installed packages in search path (default)"),
+ (" -o, --overlay-tree", "include overlays in search path"),
+ (" -p, --portage-tree", "include entire portage tree in search path")
+ ))
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ # Parse module options
+ opts = (x[0] for x in module_opts)
+ for opt in opts:
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-i', '--installed'):
+ QUERY_OPTS['includeInstalled'] = True
+ elif opt in ('-p', '--portage-tree'):
+ QUERY_OPTS['includePortTree'] = True
+ elif opt in ('-o', '--overlay-tree'):
+ QUERY_OPTS['includeOverlayTree'] = True
+
+
+def print_sequence(seq):
+ """Print every item of a sequence."""
+
+ for item in seq:
+ print item
+
+
+def sort_by_location(query, matches):
+ """Take a list of packages and sort them by location.
+
+ @rtype: tuple
+ @return:
+ installed: list of all packages in matches that are in the vdb
+ overlay: list of all packages in matches that reside in an overlay
+ porttree: list of all packages that are not in the vdb or an overlay
+ """
+
+ all_installed_packages = set()
+ if QUERY_OPTS["includeInstalled"]:
+ all_installed_packages = set(Package(x) for x in get_installed_cpvs())
+
+ # Cache package sets
+ installed = []
+ overlay = []
+ porttree = []
+
+ for pkg in matches:
+ useflags = [f.lstrip("+-") for f in pkg.get_env_var("IUSE").split()]
+ if query not in useflags:
+ continue
+
+ if QUERY_OPTS["includeInstalled"]:
+ if pkg in all_installed_packages:
+ installed.append(pkg)
+ continue
+ if pkg.is_overlay():
+ if QUERY_OPTS["includeOverlayTree"]:
+ overlay.append(pkg)
+ continue
+ if QUERY_OPTS["includePortTree"]:
+ porttree.append(pkg)
+
+ return installed, overlay, porttree
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hiIpo"
+ long_opts = ('help', 'installed', 'exclude-installed', 'portage-tree',
+ 'overlay-tree')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+ elif not (QUERY_OPTS['includeInstalled'] or
+ QUERY_OPTS['includePortTree'] or QUERY_OPTS['includeOverlayTree']):
+ # Got queries but no search path; set a sane default
+ QUERY_OPTS['includeInstalled'] = True
+
+ matches = do_lookup("*", QUERY_OPTS)
+ matches.sort()
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ if not Config["piping"]:
+ print " * Searching for USE flag %s ... " % pp.useflag(query)
+
+ installed, overlay, porttree = sort_by_location(query, matches)
+
+ if QUERY_OPTS["includeInstalled"]:
+ print " * installed packages:"
+ if not Config["piping"]:
+ installed = format_package_names(installed, 1)
+ print_sequence(installed)
+
+ if QUERY_OPTS["includePortTree"]:
+ portdir = pp.path(gentoolkit.settings["PORTDIR"])
+ print " * Portage tree (%s):" % portdir
+ if not Config["piping"]:
+ porttree = format_package_names(porttree, 2)
+ print_sequence(porttree)
+
+ if QUERY_OPTS["includeOverlayTree"]:
+ portdir_overlay = pp.path(gentoolkit.settings["PORTDIR_OVERLAY"])
+ print " * overlay tree (%s):" % portdir_overlay
+ if not Config["piping"]:
+ overlay = format_package_names(overlay, 3)
+ print_sequence(overlay)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/list_.py b/pym/gentoolkit/equery/list_.py
new file mode 100644
index 0000000..20cd376
--- /dev/null
+++ b/pym/gentoolkit/equery/list_.py
@@ -0,0 +1,251 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""List installed packages matching the query pattern"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_options, format_package_names, \
+ mod_usage, Config
+from gentoolkit.helpers2 import do_lookup, get_installed_cpvs
+from gentoolkit.package import Package
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "duplicates": False,
+ "includeInstalled": False,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False,
+ "printMatchInfo": True
+}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ # Deprecation warning added 04/09: djanderson
+ pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.")
+ pp.print_warn("-e, --exact-name is now the default behavior.")
+ pp.print_warn("Use globbing to simulate the old behavior (see man equery).")
+ pp.print_warn("Use '*' to check all installed packages.")
+ print
+
+ print mod_usage(mod_name="list")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -c, --category CAT", "only search in the category CAT"),
+ (" -d, --duplicates", "list only installed duplicate packages"),
+ (" -f, --full-regex", "query is a regular expression"),
+ (" -i, --installed", "list installed packages matching query"),
+ (" -o, --overlay-tree", "list packages in overlays"),
+ (" -p, --portage-tree", "list packages in the main portage tree")
+ ))
+
+
+def adjust_query_environment(queries):
+ """Make sure the search environment is good to go."""
+
+ if not queries and not (QUERY_OPTS["duplicates"] or
+ QUERY_OPTS["includeInstalled"] or QUERY_OPTS["includePortTree"] or
+ QUERY_OPTS["includeOverlayTree"]):
+ print_help()
+ sys.exit(2)
+ elif queries and not (QUERY_OPTS["duplicates"] or
+ QUERY_OPTS["includeInstalled"] or QUERY_OPTS["includePortTree"] or
+ QUERY_OPTS["includeOverlayTree"]):
+ QUERY_OPTS["includeInstalled"] = True
+ elif not queries and (QUERY_OPTS["duplicates"] or
+ QUERY_OPTS["includeInstalled"] or QUERY_OPTS["includePortTree"] or
+ QUERY_OPTS["includeOverlayTree"]):
+ queries = ["*"]
+
+ # Only search installed packages when listing duplicate packages
+ if QUERY_OPTS["duplicates"]:
+ QUERY_OPTS["includeInstalled"] = True
+ QUERY_OPTS["includePortTree"] = False
+ QUERY_OPTS["includeOverlayTree"] = False
+
+ return queries
+
+
+def get_duplicates(matches):
+ """Return only packages that have more than one version installed."""
+
+ dups = {}
+ result = []
+ for pkg in matches:
+ if pkg.key in dups:
+ dups[pkg.key].append(pkg)
+ else:
+ dups[pkg.key] = [pkg]
+
+ for cpv in dups.values():
+ if len(cpv) > 1:
+ result.extend(cpv)
+
+ return result
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-a', '--all'):
+ QUERY_OPTS['listAllPackages'] = True
+ elif opt in ('-c', '--category'):
+ QUERY_OPTS['categoryFilter'] = posarg
+ elif opt in ('-i', '--installed'):
+ QUERY_OPTS['includeInstalled'] = True
+ elif opt in ('-p', '--portage-tree'):
+ QUERY_OPTS['includePortTree'] = True
+ elif opt in ('-o', '--overlay-tree'):
+ QUERY_OPTS['includeOverlayTree'] = True
+ elif opt in ('-f', '--full-regex'):
+ QUERY_OPTS['isRegex'] = True
+ elif opt in ('-e', '--exact-name'):
+ pp.print_warn("-e, --exact-name is now default.")
+ pp.print_warn("Use globbing to simulate the old behavior.")
+ print
+ elif opt in ('-d', '--duplicates'):
+ QUERY_OPTS['duplicates'] = True
+
+
+def print_sequence(seq):
+ """Print every item of a sequence."""
+
+ for item in seq:
+ print item
+
+
+def sort_by_location(matches):
+ """Take a list of packages and sort them by location.
+
+ @rtype: tuple
+ @return:
+ installed: list of all packages in matches that are in the vdb
+ overlay: list of all packages in matches that reside in an overlay
+ porttree: list of all packages that are not in the vdb or an overlay
+ """
+
+ all_installed_packages = set()
+ if QUERY_OPTS["includeInstalled"]:
+ all_installed_packages = set(Package(x) for x in get_installed_cpvs())
+
+ # Cache package sets
+ installed = []
+ overlay = []
+ porttree = []
+
+ for pkg in matches:
+ if QUERY_OPTS["includeInstalled"]:
+ if pkg in all_installed_packages:
+ installed.append(pkg)
+ continue
+ if pkg.is_overlay():
+ if QUERY_OPTS["includeOverlayTree"]:
+ overlay.append(pkg)
+ continue
+ if QUERY_OPTS["includePortTree"]:
+ porttree.append(pkg)
+
+ return installed, overlay, porttree
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hc:defiIop" # -I was used to turn off -i when it was
+ # the default action, -e is now default
+
+ # 04/09: djanderson
+ # --exclude-installed is no longer needed. Kept for compatibility.
+ # --exact-name is no longer needed. Kept for compatibility.
+ long_opts = ('help', 'all', 'category=', 'installed', 'exclude-installed',
+ 'portage-tree', 'overlay-tree', 'full-regex', 'exact-name', 'duplicates')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+ queries = adjust_query_environment(queries)
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ # Find duplicate packages
+ if QUERY_OPTS["duplicates"]:
+ matches = get_duplicates(matches)
+
+ matches.sort()
+
+ installed, overlay, porttree = sort_by_location(matches)
+
+ #
+ # Output
+ #
+
+ if QUERY_OPTS["includeInstalled"]:
+ print " * installed packages:"
+ if not Config["piping"]:
+ installed = format_package_names(installed, 1)
+ print_sequence(installed)
+
+ if QUERY_OPTS["includePortTree"]:
+ portdir = pp.path(gentoolkit.settings["PORTDIR"])
+ print " * Portage tree (%s):" % portdir
+ if not Config["piping"]:
+ porttree = format_package_names(porttree, 2)
+ print_sequence(porttree)
+
+ if QUERY_OPTS["includeOverlayTree"]:
+ portdir_overlay = pp.path(gentoolkit.settings["PORTDIR_OVERLAY"])
+ print " * overlay tree (%s):" % portdir_overlay
+ if not Config["piping"]:
+ overlay = format_package_names(overlay, 3)
+ print_sequence(overlay)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/meta.py b/pym/gentoolkit/equery/meta.py
new file mode 100644
index 0000000..34dde68
--- /dev/null
+++ b/pym/gentoolkit/equery/meta.py
@@ -0,0 +1,533 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+#
+# $Header: $
+
+"""Display metadata about a given package"""
+
+# Move to Imports section after Python-2.6 is stable
+from __future__ import with_statement
+
+__author__ = "Douglas Anderson"
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import re
+import sys
+import xml.etree.cElementTree as ET
+from getopt import gnu_getopt, GetoptError
+
+from portage import settings
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, Config
+from gentoolkit.helpers2 import find_packages
+from gentoolkit.textwrap_ import TextWrapper
+
+# =======
+# Globals
+# =======
+
+# E1101: Module 'portage.output' has no $color member
+# portage.output creates color functions dynamically
+# pylint: disable-msg=E1101
+
+QUERY_OPTS = {
+ "current": False,
+ "description": False,
+ "herd": False,
+ "maintainer": False,
+ "useflags": False,
+ "upstream": False,
+ "xml": False
+}
+
+# Get the location of the main Portage tree
+PORTDIR = [settings["PORTDIR"] or os.path.join(os.sep, "usr", "portage")]
+# Check for overlays
+if settings["PORTDIR_OVERLAY"]:
+ PORTDIR.extend(settings["PORTDIR_OVERLAY"].split())
+
+if not Config["piping"] and Config["verbosityLevel"] >= 3:
+ VERBOSE = True
+else:
+ VERBOSE = False
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="meta")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -c, --current", "parse metadata.xml in the current directory"),
+ (" -d, --description", "show an extended package description"),
+ (" -H, --herd", "show the herd(s) for the package"),
+ (" -m, --maintainer", "show the maintainer(s) for the package"),
+ (" -u, --useflags", "show per-package USE flag descriptions"),
+ (" -U, --upstream", "show package's upstream information"),
+ (" -x, --xml", "show the plain XML file")
+ ))
+
+
+def call_get_functions(xml_tree, meta, got_opts):
+ """Call information gathering funtions and display the results."""
+
+ if QUERY_OPTS["herd"] or not got_opts:
+ herd = get_herd(xml_tree)
+ if QUERY_OPTS["herd"]:
+ herd = format_list(herd)
+ else:
+ herd = format_list(herd, "Herd: ", " " * 13)
+ print_sequence(herd)
+
+ if QUERY_OPTS["maintainer"] or not got_opts:
+ maint = get_maitainer(xml_tree)
+ if QUERY_OPTS["maintainer"]:
+ maint = format_list(maint)
+ else:
+ maint = format_list(maint, "Maintainer: ", " " * 13)
+ print_sequence(maint)
+
+ if QUERY_OPTS["upstream"] or not got_opts:
+ upstream = get_upstream(xml_tree)
+ if QUERY_OPTS["upstream"]:
+ upstream = format_list(upstream)
+ else:
+ upstream = format_list(upstream, "Upstream: ", " " * 13)
+ print_sequence(upstream)
+
+ if QUERY_OPTS["description"]:
+ desc = get_description(xml_tree)
+ print_sequence(format_list(desc))
+
+ if QUERY_OPTS["useflags"]:
+ useflags = get_useflags(xml_tree)
+ print_sequence(format_list(useflags))
+
+ if QUERY_OPTS["xml"]:
+ print_file(meta)
+
+
+def format_line(line, first="", subsequent="", force_quiet=False):
+ """Wrap a string at word boundaries and optionally indent the first line
+ and/or subsequent lines with custom strings.
+
+ Preserve newlines if the longest line is not longer than
+ Config['termWidth']. To force the preservation of newlines and indents,
+ split the string into a list and feed it to format_line via format_list.
+
+ @see: format_list()
+ @type line: string
+ @param line: text to format
+ @type first: string
+ @param first: text to prepend to the first line
+ @type subsequent: string
+ @param subsequent: text to prepend to subsequent lines
+ @type force_quiet: boolean
+ @rtype: string
+ @return: A wrapped line
+ """
+
+ if line:
+ line = line.expandtabs().strip("\n").splitlines()
+ else:
+ if force_quiet:
+ return
+ else:
+ return first + "None specified"
+
+ if len(first) > len(subsequent):
+ wider_indent = first
+ else:
+ wider_indent = subsequent
+
+ widest_line_len = len(max(line, key=len)) + len(wider_indent)
+
+ if widest_line_len > Config['termWidth']:
+ twrap = TextWrapper(width=Config['termWidth'], expand_tabs=False,
+ initial_indent=first, subsequent_indent=subsequent)
+ line = " ".join(line)
+ line = re.sub("\s+", " ", line)
+ line = line.lstrip()
+ result = twrap.fill(line)
+ else:
+ # line will fit inside Config['termWidth'], so preserve whitespace and
+ # newlines
+ line[0] = first + line[0] # Avoid two newlines if len == 1
+
+ if len(line) > 1:
+ line[0] = line[0] + "\n"
+ for i in range(1, (len(line[1:-1]) + 1)):
+ line[i] = subsequent + line[i] + "\n"
+ line[-1] = subsequent + line[-1] # Avoid two newlines on last line
+
+ if line[-1].isspace():
+ del line[-1] # Avoid trailing blank lines
+
+ result = "".join(line)
+
+ return result.encode("utf-8")
+
+
+def format_list(lst, first="", subsequent="", force_quiet=False):
+ """Feed elements of a list to format_line().
+
+ @see: format_line()
+ @type lst: list
+ @param lst: list to format
+ @type first: string
+ @param first: text to prepend to the first line
+ @type subsequent: string
+ @param subsequent: text to prepend to subsequent lines
+ @rtype: list
+ @return: list with element text wrapped at Config['termWidth']
+ """
+
+ result = []
+ if lst:
+ # Format the first line
+ line = format_line(lst[0], first, subsequent, force_quiet)
+ result.append(line)
+ # Format subsequent lines
+ for elem in lst[1:]:
+ if elem:
+ result.append(format_line(elem, subsequent, subsequent,
+ force_quiet))
+ else:
+ # We don't want to send a blank line to format_line()
+ result.append("")
+ else:
+ if VERBOSE:
+ if force_quiet:
+ result = None
+ else:
+ # Send empty list, we'll get back first + `None specified'
+ result.append(format_line(lst, first, subsequent))
+
+ return result
+
+
+def get_herd(xml_tree):
+ """Return a list of text nodes for <herd>."""
+
+ return [e.text for e in xml_tree.findall("herd")]
+
+
+def get_description(xml_tree):
+ """Return a list of text nodes for <longdescription>.
+
+ @todo: Support the `lang' attribute
+ """
+
+ return [e.text for e in xml_tree.findall("longdescription")]
+
+
+def get_maitainer(xml_tree):
+ """Return a parsable tree of all maintainer elements and sub-elements."""
+
+ first_run = True
+ result = []
+ for node in xml_tree.findall("maintainer"):
+ if not first_run:
+ result.append("")
+ restrict = node.get("restrict")
+ if restrict:
+ result.append("(%s %s)" %
+ (pp.emph("Restrict to"), pp.output.green(restrict)))
+ result.extend(e.text for e in node)
+ first_run = False
+
+ return result
+
+
+def get_overlay_name(p_dir):
+ """Determine the overlay name and return a formatted string."""
+
+ result = []
+ cat_pkg = '/'.join(p_dir.split('/')[-2:])
+ result.append(" * %s" % pp.cpv(cat_pkg))
+ o_dir = '/'.join(p_dir.split('/')[:-2])
+ if o_dir != PORTDIR[0]:
+ # o_dir is an overlay
+ o_name = o_dir.split('/')[-1]
+ o_name = ("[", o_name, "]")
+ result.append(pp.output.turquoise("".join(o_name)))
+
+ return ' '.join(result)
+
+
+def get_package_directory(queries):
+ """Find a package's portage directory."""
+
+ # Find queries' Portage directory and throw error if invalid
+ if not QUERY_OPTS["current"]:
+ # We need at least one program name to run
+ if not queries:
+ print_help()
+ sys.exit(2)
+ else:
+ package_dir = []
+ for query in queries:
+ matches = find_packages(query, include_masked=True)
+ # Prefer a package that's in the Portage tree over one in an
+ # overlay. Start with oldest first.
+ pkg = None
+ while reversed(matches):
+ pkg = matches.pop()
+ if not pkg.is_overlay():
+ break
+ if pkg:
+ package_dir.append(pkg.get_package_path())
+ else:
+ package_dir = [os.getcwd()]
+
+ return package_dir
+
+
+def get_useflags(xml_tree):
+ """Return a list of formatted <useflag> lines, including blank elements
+ where blank lines should be printed."""
+
+ first_run = True
+ result = []
+ for node in xml_tree.getiterator("flag"):
+ if not first_run:
+ result.append("")
+ flagline = pp.useflag(node.get("name"))
+ restrict = node.get("restrict")
+ if restrict:
+ result.append("%s (%s %s)" %
+ (flagline, pp.emph("Restrict to"), pp.output.green(restrict)))
+ else:
+ result.append(flagline)
+ # ElementTree handles nested element text in a funky way.
+ # So we need to dump the raw XML and parse it manually.
+ flagxml = ET.tostring(node)
+ flagxml = re.sub("\s+", " ", flagxml)
+ flagxml = re.sub("\n\t", "", flagxml)
+ flagxml = re.sub("<(pkg|cat)>(.*?)</(pkg|cat)>",
+ pp.cpv(r"\2"), flagxml)
+ flagtext = re.sub("<.*?>", "", flagxml)
+ result.append(flagtext)
+ first_run = False
+
+ return result
+
+
+def _get_upstream_bugtracker(node):
+ """WRITE IT"""
+
+ bt_loc = [e.text for e in node.findall("bugs-to")]
+
+ return format_list(bt_loc, "Bugs to: ", " " * 12, force_quiet=True)
+
+
+def _get_upstream_changelog(node):
+ """WRITE IT"""
+
+ cl_paths = [e.text for e in node.findall("changelog")]
+
+ return format_list(cl_paths, "Changelog: ", " " * 12, force_quiet=True)
+
+
+def _get_upstream_documentation(node):
+ """WRITE IT"""
+
+ doc = []
+ for elem in node.findall("doc"):
+ lang = elem.get("lang")
+ if lang:
+ lang = "(%s)" % pp.output.yellow(lang)
+ else:
+ lang = ""
+ doc.append(" ".join([elem.text, lang]))
+
+ return format_list(doc, "Docs: ", " " * 12, force_quiet=True)
+
+
+def _get_upstream_maintainer(node):
+ """WRITE IT"""
+
+ maintainer = node.findall("maintainer")
+ maint = []
+ for elem in maintainer:
+ name = elem.find("name")
+ email = elem.find("email")
+ if elem.get("status") == "active":
+ status = "(%s)" % pp.output.green("active")
+ elif elem.get("status") == "inactive":
+ status = "(%s)" % pp.output.red("inactive")
+ elif elem.get("status"):
+ status = "(" + elem.get("status") + ")"
+ else:
+ status = ""
+ maint.append(" ".join([name.text, email.text, status]))
+
+ return format_list(maint, "Maintainer: ", " " * 12, force_quiet=True)
+
+
+def _get_upstream_remoteid(node):
+ """WRITE IT"""
+
+ r_id = [e.get("type") + ": " + e.text for e in node.findall("remote-id")]
+
+ return format_list(r_id, "Remote ID: ", " " * 12, force_quiet=True)
+
+
+def get_upstream(xml_tree):
+ """Return a list of formatted <upstream> lines, including blank elements
+ where blank lines should be printed."""
+
+ first_run = True
+ result = []
+ for node in xml_tree.findall("upstream"):
+ if not first_run:
+ result.append("")
+
+ maint = _get_upstream_maintainer(node)
+ if maint:
+ result.append("\n".join(maint))
+
+ changelog = _get_upstream_changelog(node)
+ if changelog:
+ result.append("\n".join(changelog))
+
+ documentation = _get_upstream_documentation(node)
+ if documentation:
+ result.append("\n".join(documentation))
+
+ bugs_to = _get_upstream_bugtracker(node)
+ if bugs_to:
+ result.append("\n".join(bugs_to))
+
+ remote_id = _get_upstream_remoteid(node)
+ if remote_id:
+ result.append("\n".join(remote_id))
+
+ first_run = False
+
+ return result
+
+
+def print_sequence(seq):
+ """Print each element of a sequence."""
+
+ for elem in seq:
+ print elem
+
+
+def uniqify(seq, preserve_order=True):
+ """Return a uniqified list. Optionally preserve order."""
+
+ if preserve_order:
+ seen = set()
+ result = [x for x in seq if x not in seen and not seen.add(x)]
+ else:
+ result = list(set(seq))
+
+ return result
+
+
+def print_file(path):
+ """Display the contents of a file."""
+
+ with open(path) as open_file:
+ lines = open_file.read()
+ print lines.strip()
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ for opt in opts:
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-c', '--current'):
+ QUERY_OPTS["current"] = True
+ elif opt in ('-d', '--description'):
+ QUERY_OPTS["description"] = True
+ elif opt in ('-H', '--herd'):
+ QUERY_OPTS["herd"] = True
+ elif opt in ('-m', '--maintainer'):
+ QUERY_OPTS["maintainer"] = True
+ elif opt in ('-u', '--useflags'):
+ QUERY_OPTS["useflags"] = True
+ elif opt in ('-U', '--upstream'):
+ QUERY_OPTS["upstream"] = True
+ elif opt in ('-x', '--xml'):
+ QUERY_OPTS["xml"] = True
+
+
+def main(input_args):
+ """Parse input and run the program."""
+
+ short_opts = "hcdHmuUx"
+ long_opts = ('help', 'current', 'description', 'herd', 'maintainer',
+ 'useflags', 'upstream', 'xml')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ package_dir = get_package_directory(queries)
+ if not package_dir:
+ raise errors.GentoolkitNoMatches(queries)
+
+ metadata_path = [os.path.join(d, "metadata.xml") for d in package_dir]
+
+ # --------------------------------
+ # Check options and call functions
+ # --------------------------------
+
+ first_run = True
+ for p_dir, meta in zip(package_dir, metadata_path):
+ if not first_run:
+ print
+
+ if VERBOSE:
+ print get_overlay_name(p_dir)
+
+ try:
+ xml_tree = ET.parse(meta)
+ except IOError:
+ pp.print_error("No metadata available")
+ first_run = False
+ continue
+
+ got_opts = False
+ if (QUERY_OPTS["herd"] or QUERY_OPTS["description"] or
+ QUERY_OPTS["useflags"] or QUERY_OPTS["maintainer"] or
+ QUERY_OPTS["upstream"] or QUERY_OPTS["xml"]):
+ # Specific information requested, less formatting
+ got_opts = True
+
+ call_get_functions(xml_tree, meta, got_opts)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/size.py b/pym/gentoolkit/equery/size.py
new file mode 100644
index 0000000..59f45d8
--- /dev/null
+++ b/pym/gentoolkit/equery/size.py
@@ -0,0 +1,199 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Print total size of files contained in a given package"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit.equery import format_options, mod_usage, Config
+from gentoolkit.helpers2 import do_lookup
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {
+ "categoryFilter": None,
+ "includeInstalled": False,
+ "includePortTree": False,
+ "includeOverlayTree": False,
+ "includeMasked": True,
+ "isRegex": False,
+ "matchExact": False,
+ "printMatchInfo": False,
+ "sizeInBytes": False
+}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+
+ # Deprecation warning added 04/09: djanderson
+ pp.print_warn("Default action for this module has changed in Gentoolkit 0.3.")
+ pp.print_warn("-e, --exact-name is now the default behavior.")
+ pp.print_warn("Use globbing to simulate the old behavior (see man equery).")
+ pp.print_warn("Use '*' to check all installed packages.")
+ print
+
+ print mod_usage(mod_name="size")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -b, --bytes", "report size in bytes"),
+ (" -c, --category CAT", "only search in the category CAT"),
+ (" -f, --full-regex", "query is a regular expression")
+ ))
+
+
+def display_size(match_set):
+ """Display the total size of all accessible files owned by packages.
+
+ @type match_set: list
+ @param match_set: package cat/pkg-ver strings
+ """
+
+ for pkg in match_set:
+ (size, files, uncounted) = pkg.size()
+
+ if Config["piping"]:
+ info = "%s: total(%d), inaccessible(%d), size(%s)"
+ print info % (pkg.cpv, files, uncounted, size)
+ else:
+ print " * %s" % pp.cpv(pkg.cpv)
+ print "Total files : %s".rjust(25) % pp.number(str(files))
+
+ if uncounted:
+ pp.print_info(0, "Inaccessible files : %s".rjust(25) %
+ pp.number(str(uncounted)))
+
+ if QUERY_OPTS["sizeInBytes"]:
+ size_str = pp.number(str(size))
+ else:
+ size_str = "%s %s" % format_bytes(size)
+
+ pp.print_info(0, "Total size : %s".rjust(25) % size_str)
+
+
+def format_bytes(bytes_, precision=2):
+ """Format bytes into human-readable format (IEC naming standard).
+
+ @see: http://mail.python.org/pipermail/python-list/2008-August/503423.html
+ @rtype: tuple
+ @return: (str(num), str(label))
+ """
+
+ labels = (
+ (1<<40L, 'TiB'),
+ (1<<30L, 'GiB'),
+ (1<<20L, 'MiB'),
+ (1<<10L, 'KiB'),
+ (1, 'bytes')
+ )
+
+ if bytes_ == 0:
+ return (pp.number('0'), 'bytes')
+ elif bytes_ == 1:
+ return (pp.number('1'), 'byte')
+
+ for factor, label in labels:
+ if not bytes_ >= factor:
+ continue
+
+ float_split = str(bytes_/float(factor)).split('.')
+ integer = float_split[0]
+ decimal = float_split[1]
+ if int(decimal[0:precision]):
+ float_string = '.'.join([integer, decimal[0:precision]])
+ else:
+ float_string = integer
+
+ return (pp.number(float_string), label)
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ posargs = (x[1] for x in module_opts)
+ for opt, posarg in zip(opts, posargs):
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-b', '--bytes'):
+ QUERY_OPTS["sizeInBytes"] = True
+ elif opt in ('-c', '--category'):
+ QUERY_OPTS['categoryFilter'] = posarg
+ elif opt in ('-e', '--exact-name'):
+ pp.print_warn("-e, --exact-name is now default.")
+ pp.print_warn("Use globbing to simulate the old behavior.")
+ print
+ elif opt in ('-f', '--full-regex'):
+ QUERY_OPTS['isRegex'] = True
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ # -e, --exact-name is no longer needed. Kept for compatibility.
+ # 04/09 djanderson
+ short_opts = "hbc:fe"
+ long_opts = ('help', 'bytes', 'category=', 'full-regex', 'exact-name')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries and not QUERY_OPTS["includeInstalled"]:
+ print_help()
+ sys.exit(2)
+ elif queries and not QUERY_OPTS["includeInstalled"]:
+ QUERY_OPTS["includeInstalled"] = True
+ elif QUERY_OPTS["includeInstalled"]:
+ queries = ["*"]
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ matches = do_lookup(query, QUERY_OPTS)
+
+ if not matches:
+ pp.print_error("No package found matching %s" % query)
+
+ display_size(matches)
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/uses.py b/pym/gentoolkit/equery/uses.py
new file mode 100644
index 0000000..2718613
--- /dev/null
+++ b/pym/gentoolkit/equery/uses.py
@@ -0,0 +1,340 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Display USE flags for a given package"""
+
+# Move to imports section when Python 2.6 is stable
+from __future__ import with_statement
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import re
+import sys
+from getopt import gnu_getopt, GetoptError
+from glob import glob
+import xml.etree.cElementTree as ET
+
+from portage.util import unique_array
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage, Config
+from gentoolkit.helpers2 import compare_package_strings, find_best_match, \
+ find_packages
+from gentoolkit.textwrap_ import TextWrapper
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {"allVersions" : False}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name=__name__.split('.')[-1])
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -a, --all", "include all package versions")
+ ))
+
+
+def display_useflags(output):
+ """Print USE flag descriptions and statuses.
+
+ @type output: list
+ @param output: [(inuse, inused, flag, desc, restrict), ...]
+ inuse (int) = 0 or 1; if 1, flag is set in make.conf
+ inused (int) = 0 or 1; if 1, package is installed with flag enabled
+ flag (str) = the name of the USE flag
+ desc (str) = the flag's description
+ restrict (str) = corresponds to the text of restrict in metadata
+ """
+
+ maxflag_len = len(max([t[2] for t in output], key=len))
+
+ twrap = TextWrapper()
+ twrap.width = Config['termWidth']
+ twrap.subsequent_indent = " " * (maxflag_len + 8)
+
+ markers = ("-", "+")
+ color = [pp.useflagoff, pp.useflagon]
+ for in_makeconf, in_installed, flag, desc, restrict in output:
+ if Config["piping"]:
+ pp.print_info(0, markers[in_makeconf] + flag)
+ else:
+ flag_name = ""
+ if in_makeconf != in_installed:
+ flag_name += pp.emph(" %s %s" %
+ (markers[in_makeconf], markers[in_installed]))
+ else:
+ flag_name += (" %s %s" %
+ (markers[in_makeconf], markers[in_installed]))
+
+ flag_name += " " + color[in_makeconf](flag.ljust(maxflag_len))
+ flag_name += " : "
+
+ # print description
+ if restrict:
+ restrict = "(%s %s)" % (pp.emph("Restricted to"),
+ pp.cpv(restrict))
+ twrap.initial_indent = flag_name
+ pp.print_info(0, twrap.fill(restrict))
+ if desc:
+ twrap.initial_indent = twrap.subsequent_indent
+ pp.print_info(0, twrap.fill(desc))
+ else:
+ pp.print_info(0, " : <unknown>")
+ else:
+ if desc:
+ twrap.initial_indent = flag_name
+ desc = twrap.fill(desc)
+ pp.print_info(0, desc)
+ else:
+ twrap.initial_indent = flag_name
+ pp.print_info(0, twrap.fill("<unknown>"))
+
+
+def get_global_useflags():
+ """Get global and expanded USE flag variables from
+ PORTDIR/profiles/use.desc and PORTDIR/profiles/desc/*.desc respectively.
+
+ @rtype: dict
+ @return: {'flag_name': 'flag description', ...}
+ """
+
+ global_usedesc = {}
+ # Get global USE flag descriptions
+ try:
+ path = os.path.join(gentoolkit.settings["PORTDIR"], 'profiles',
+ 'use.desc')
+ with open(path) as open_file:
+ for line in open_file:
+ if line.startswith('#'):
+ continue
+ # Ex. of fields: ['syslog', 'Enables support for syslog\n']
+ fields = line.split(" - ", 1)
+ if len(fields) == 2:
+ global_usedesc[fields[0]] = fields[1].rstrip()
+ except IOError:
+ pp.print_warn("Could not load USE flag descriptions from %s" %
+ pp.path(path))
+
+ del path, open_file
+ # Add USE_EXPANDED variables to usedesc hash -- Bug #238005
+ for path in glob(os.path.join(gentoolkit.settings["PORTDIR"],
+ 'profiles', 'desc', '*.desc')):
+ try:
+ with open(path) as open_file:
+ for line in open_file:
+ if line.startswith('#'):
+ continue
+ fields = [field.strip() for field in line.split(" - ", 1)]
+ if len(fields) == 2:
+ expanded_useflag = "%s_%s" % \
+ (path.split("/")[-1][0:-5], fields[0])
+ global_usedesc[expanded_useflag] = fields[1]
+ except IOError:
+ pp.print_warn("Could not load USE flag descriptions from %s" %
+ path)
+
+ return global_usedesc
+
+
+def get_local_useflags(pkg):
+ """Parse package-specific flag descriptions from a package's metadata.xml.
+
+ @see: http://www.gentoo.org/proj/en/glep/glep-0056.html
+ @type pkg: gentoolkit.package.Package
+ @param pkg: the package to find useflags for
+ @rtype: dict
+ @return: {string: tuple}
+ string = flag's name
+ tuple = (description, restrictions)
+ """
+
+ result = {}
+
+ metadata = os.path.join(pkg.get_package_path(), 'metadata.xml')
+ try:
+ xml_tree = ET.parse(metadata)
+ except IOError:
+ pp.print_error("Could not open %s" % metadata)
+ return result
+
+ for node in xml_tree.getiterator("flag"):
+ name = node.get("name")
+ restrict = node.get("restrict")
+ # ElementTree handles nested element text in a funky way.
+ # So we need to dump the raw XML and parse it manually.
+ flagxml = ET.tostring(node)
+ flagxml = re.sub("\s+", " ", flagxml)
+ flagxml = re.sub("\n\t", "", flagxml)
+ flagxml = re.sub("<(pkg|cat)>([^<]*)</(pkg|cat)>",
+ pp.cpv("%s" % r"\2"), flagxml)
+ flagtext = re.sub("<.*?>", "", flagxml)
+ result[name] = (flagtext, restrict)
+
+ return result
+
+
+def get_matches(query):
+ """Get packages matching query."""
+
+ if not QUERY_OPTS["allVersions"]:
+ matches = [find_best_match(query)]
+ if None in matches:
+ matches = find_packages(query, include_masked=False)
+ if matches:
+ matches = sorted(matches, compare_package_strings)[-1:]
+ else:
+ matches = find_packages(query, include_masked=True)
+
+ if not matches:
+ raise errors.GentoolkitNoMatches(query)
+
+ return matches
+
+
+def get_output_descriptions(pkg, global_usedesc):
+ """Prepare descriptions and usage information for each USE flag."""
+
+ local_usedesc = get_local_useflags(pkg)
+ iuse = pkg.get_env_var("IUSE")
+
+ if iuse:
+ usevar = unique_array([x.lstrip('+-') for x in iuse.split()])
+ usevar.sort()
+ else:
+ usevar = []
+
+ if pkg.is_installed():
+ used_flags = pkg.get_use_flags().split()
+ else:
+ used_flags = gentoolkit.settings["USE"].split()
+
+ # store (inuse, inused, flag, desc, restrict)
+ output = []
+ for flag in usevar:
+ inuse = False
+ inused = False
+ try:
+ desc = local_usedesc[flag][0]
+ except KeyError:
+ try:
+ desc = global_usedesc[flag]
+ except KeyError:
+ desc = ""
+ try:
+ restrict = local_usedesc[flag][1]
+ except KeyError:
+ restrict = ""
+
+ if flag in pkg.get_settings("USE").split():
+ inuse = True
+ if flag in used_flags:
+ inused = True
+
+ output.append((inuse, inused, flag, desc, restrict))
+
+ return output
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ for opt in opts:
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-a', '--all'):
+ QUERY_OPTS['allVersions'] = True
+
+
+def print_legend(query):
+ """Print a legend to explain the output format."""
+
+ if not Config['piping']:
+ pp.print_info(3, " * Searching for packages matching %s ..." %
+ pp.pkgquery(query))
+ pp.print_info(3, "[ Legend : %s - flag is set in make.conf ]"
+ % pp.emph("U"))
+ pp.print_info(3, "[ : %s - package is installed with flag ]"
+ % pp.emph("I"))
+ pp.print_info(3, "[ Colors : %s, %s ]" %
+ (pp.useflagon("set"), pp.useflagoff("unset")))
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "ha"
+ long_opts = ('help', 'all')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ #
+ # Output
+ #
+
+ first_run = True
+ for query in queries:
+ if not first_run:
+ print
+
+ print_legend(query)
+
+ matches = get_matches(query)
+ matches.sort()
+
+ global_usedesc = get_global_useflags()
+ for pkg in matches:
+
+ output = get_output_descriptions(pkg, global_usedesc)
+ if output:
+ if not Config['piping']:
+ pp.print_info(3, "[ Found these USE flags for %s ]" %
+ pp.cpv(pkg.cpv))
+ pp.print_info(3, pp.emph(" U I"))
+ display_useflags(output)
+ else:
+ if not Config['piping']:
+ pp.print_info(3, "[ No USE flags found for %s ]" %
+ pp.cpv(pkg.cpv))
+
+ first_run = False
diff --git a/pym/gentoolkit/equery/which.py b/pym/gentoolkit/equery/which.py
new file mode 100644
index 0000000..4cae712
--- /dev/null
+++ b/pym/gentoolkit/equery/which.py
@@ -0,0 +1,98 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header: $
+
+"""Display the path to the ebuild that would be used by Portage with the current
+configuration
+"""
+
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import os
+import sys
+from getopt import gnu_getopt, GetoptError
+
+import gentoolkit.pprinter as pp
+from gentoolkit import errors
+from gentoolkit.equery import format_options, mod_usage
+from gentoolkit.helpers2 import find_packages
+
+# =======
+# Globals
+# =======
+
+QUERY_OPTS = {"includeMasked": False}
+
+# =========
+# Functions
+# =========
+
+def print_help(with_description=True):
+ """Print description, usage and a detailed help message.
+
+ @type with_description: bool
+ @param with_description: if true, print module's __doc__ string
+ """
+
+ if with_description:
+ print __doc__.strip()
+ print
+ print mod_usage(mod_name="which")
+ print
+ print pp.command("options")
+ print format_options((
+ (" -h, --help", "display this help message"),
+ (" -m, --include-masked", "return highest version ebuild available")
+ ))
+
+
+def parse_module_options(module_opts):
+ """Parse module options and update GLOBAL_OPTS"""
+
+ opts = (x[0] for x in module_opts)
+ for opt in opts:
+ if opt in ('-h', '--help'):
+ print_help()
+ sys.exit(0)
+ elif opt in ('-m', '--include-masked'):
+ QUERY_OPTS['includeMasked'] = True
+
+
+def main(input_args):
+ """Parse input and run the program"""
+
+ short_opts = "hm"
+ long_opts = ('help', 'include-masked')
+
+ try:
+ module_opts, queries = gnu_getopt(input_args, short_opts, long_opts)
+ except GetoptError, err:
+ pp.print_error("Module %s" % err)
+ print
+ print_help(with_description=False)
+ sys.exit(2)
+
+ parse_module_options(module_opts)
+
+ if not queries:
+ print_help()
+ sys.exit(2)
+
+ for query in queries:
+
+ matches = find_packages(query, QUERY_OPTS['includeMasked'])
+ if matches:
+ pkg = sorted(matches).pop()
+ ebuild_path = pkg.get_ebuild_path()
+ if ebuild_path:
+ pp.print_info(0, os.path.normpath(ebuild_path))
+ else:
+ pp.print_warn("No ebuilds to satisfy %s" % pkg.name)
+ else:
+ raise errors.GentoolkitNoMatches(query)
diff --git a/pym/gentoolkit/errors.py b/pym/gentoolkit/errors.py
new file mode 100644
index 0000000..c635192
--- /dev/null
+++ b/pym/gentoolkit/errors.py
@@ -0,0 +1,92 @@
+# Copyright(c) 2004-2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or later
+
+"""Exception classes for gentoolkit"""
+
+__all__ = [
+ 'FatalError',
+ 'GentoolkitException',
+ 'GentoolkitInvalidAtom',
+ 'GentoolkitInvalidCategory',
+ 'GentoolkitInvalidPackageName',
+ 'GentoolkitInvalidCPV',
+ 'GentoolkitInvalidRegex',
+ 'GentoolkitNoMatches'
+]
+
+# =======
+# Imports
+# =======
+
+import sys
+
+import gentoolkit.pprinter as pp
+
+# ==========
+# Exceptions
+# ==========
+
+class GentoolkitException(Exception):
+ """Base class for gentoolkit exceptions"""
+ def __init__(self):
+ pass
+
+
+class GentoolkitFatalError(GentoolkitException):
+ """A fatal error occurred. Usually used to catch Portage exceptions."""
+ def __init__(self, err):
+ pp.print_error("Fatal error: %s" % err)
+ sys.exit(2)
+
+
+class GentoolkitInvalidAtom(GentoolkitException):
+ """Got a malformed package atom"""
+ def __init__(self, atom):
+ pp.print_error("Invalid atom: '%s'" % atom)
+ sys.exit(2)
+
+
+class GentoolkitInvalidCategory(GentoolkitException):
+ """The category was not listed in portage.settings.categories"""
+ def __init__(self, category):
+ pp.print_error("Invalid category: '%s'" % category)
+ if not category:
+ pp.print_error("Try --category=cat1,cat2 with no spaces.")
+ sys.exit(2)
+
+
+class GentoolkitInvalidPackageName(GentoolkitException):
+ """Got an unknown package name"""
+ def __init__(self, package):
+ pp.print_error("Invalid package name: '%s'" % package)
+ sys.exit(2)
+
+
+class GentoolkitInvalidCPV(GentoolkitException):
+ """Got an unknown package name"""
+ def __init__(self, cpv):
+ pp.print_error("Invalid CPV: '%s'" % cpv)
+ sys.exit(2)
+
+
+class GentoolkitInvalidRegex(GentoolkitException):
+ """The regex could not be compiled"""
+ def __init__(self, regex):
+ pp.print_error("Invalid regex: '%s'" % regex)
+ sys.exit(2)
+
+
+class GentoolkitNoMatches(GentoolkitException):
+ """No packages were found matching the search query"""
+ def __init__(self, query):
+ pp.print_error("No packages matching '%s'" % query)
+ sys.exit(2)
+
+
+# XXX: Deprecated
+class FatalError:
+ def __init__(self, s):
+ self._message = s
+ def get_message(self):
+ return self._message
diff --git a/pym/gentoolkit/glsa/__init__.py b/pym/gentoolkit/glsa/__init__.py
new file mode 100644
index 0000000..4c8f280
--- /dev/null
+++ b/pym/gentoolkit/glsa/__init__.py
@@ -0,0 +1,644 @@
+# $Header$
+
+# This program is licensed under the GPL, version 2
+
+# WARNING: this code is only tested by a few people and should NOT be used
+# on production systems at this stage. There are possible security holes and probably
+# bugs in this code. If you test it please report ANY success or failure to
+# me (genone@gentoo.org).
+
+# The following planned features are currently on hold:
+# - getting GLSAs from http/ftp servers (not really useful without the fixed ebuilds)
+# - GPG signing/verification (until key policy is clear)
+
+__author__ = "Marius Mauch <genone@gentoo.org>"
+
+import os
+import sys
+import urllib
+import time
+import codecs
+import re
+import xml.dom.minidom
+
+if sys.version_info[0:2] < (2,3):
+ raise NotImplementedError("Python versions below 2.3 have broken XML code " \
+ +"and are not supported")
+
+try:
+ import portage
+except ImportError:
+ sys.path.insert(0, "/usr/lib/portage/pym")
+ import portage
+
+# Note: the space for rgt and rlt is important !!
+opMapping = {"le": "<=", "lt": "<", "eq": "=", "gt": ">", "ge": ">=",
+ "rge": ">=~", "rle": "<=~", "rgt": " >~", "rlt": " <~"}
+NEWLINE_ESCAPE = "!;\\n" # some random string to mark newlines that should be preserved
+SPACE_ESCAPE = "!;_" # some random string to mark spaces that should be preserved
+
+def center(text, width):
+ """
+ Returns a string containing I{text} that is padded with spaces on both
+ sides. If C{len(text) >= width} I{text} is returned unchanged.
+
+ @type text: String
+ @param text: the text to be embedded
+ @type width: Integer
+ @param width: the minimum length of the returned string
+ @rtype: String
+ @return: the expanded string or I{text}
+ """
+ if len(text) >= width:
+ return text
+ margin = (width-len(text))/2
+ rValue = " "*margin
+ rValue += text
+ if 2*margin + len(text) == width:
+ rValue += " "*margin
+ elif 2*margin + len(text) + 1 == width:
+ rValue += " "*(margin+1)
+ return rValue
+
+
+def wrap(text, width, caption=""):
+ """
+ Wraps the given text at column I{width}, optionally indenting
+ it so that no text is under I{caption}. It's possible to encode
+ hard linebreaks in I{text} with L{NEWLINE_ESCAPE}.
+
+ @type text: String
+ @param text: the text to be wrapped
+ @type width: Integer
+ @param width: the column at which the text should be wrapped
+ @type caption: String
+ @param caption: this string is inserted at the beginning of the
+ return value and the paragraph is indented up to
+ C{len(caption)}.
+ @rtype: String
+ @return: the wrapped and indented paragraph
+ """
+ rValue = ""
+ line = caption
+ text = text.replace(2*NEWLINE_ESCAPE, NEWLINE_ESCAPE+" "+NEWLINE_ESCAPE)
+ words = text.split()
+ indentLevel = len(caption)+1
+
+ for w in words:
+ if line[-1] == "\n":
+ rValue += line
+ line = " "*indentLevel
+ if len(line)+len(w.replace(NEWLINE_ESCAPE, ""))+1 > width:
+ rValue += line+"\n"
+ line = " "*indentLevel+w.replace(NEWLINE_ESCAPE, "\n")
+ elif w.find(NEWLINE_ESCAPE) >= 0:
+ if len(line.strip()) > 0:
+ rValue += line+" "+w.replace(NEWLINE_ESCAPE, "\n")
+ else:
+ rValue += line+w.replace(NEWLINE_ESCAPE, "\n")
+ line = " "*indentLevel
+ else:
+ if len(line.strip()) > 0:
+ line += " "+w
+ else:
+ line += w
+ if len(line) > 0:
+ rValue += line.replace(NEWLINE_ESCAPE, "\n")
+ rValue = rValue.replace(SPACE_ESCAPE, " ")
+ return rValue
+
+def checkconfig(myconfig):
+ """
+ takes a portage.config instance and adds GLSA specific keys if
+ they are not present. TO-BE-REMOVED (should end up in make.*)
+ """
+ mysettings = {
+ "GLSA_DIR": portage.settings["PORTDIR"]+"/metadata/glsa/",
+ "GLSA_PREFIX": "glsa-",
+ "GLSA_SUFFIX": ".xml",
+ "CHECKFILE": "/var/cache/edb/glsa",
+ "GLSA_SERVER": "www.gentoo.org/security/en/glsa/", # not completely implemented yet
+ "CHECKMODE": "local", # not completely implemented yet
+ "PRINTWIDTH": "76"
+ }
+ for k in mysettings.keys():
+ if k not in myconfig:
+ myconfig[k] = mysettings[k]
+ return myconfig
+
+def get_glsa_list(repository, myconfig):
+ """
+ Returns a list of all available GLSAs in the given repository
+ by comparing the filelist there with the pattern described in
+ the config.
+
+ @type repository: String
+ @param repository: The directory or an URL that contains GLSA files
+ (Note: not implemented yet)
+ @type myconfig: portage.config
+ @param myconfig: a GLSA aware config instance (see L{checkconfig})
+
+ @rtype: List of Strings
+ @return: a list of GLSA IDs in this repository
+ """
+ # TODO: remote fetch code for listing
+
+ rValue = []
+
+ if not os.access(repository, os.R_OK):
+ return []
+ dirlist = os.listdir(repository)
+ prefix = myconfig["GLSA_PREFIX"]
+ suffix = myconfig["GLSA_SUFFIX"]
+
+ for f in dirlist:
+ try:
+ if f[:len(prefix)] == prefix:
+ rValue.append(f[len(prefix):-1*len(suffix)])
+ except IndexError:
+ pass
+ return rValue
+
+def getListElements(listnode):
+ """
+ Get all <li> elements for a given <ol> or <ul> node.
+
+ @type listnode: xml.dom.Node
+ @param listnode: <ul> or <ol> list to get the elements for
+ @rtype: List of Strings
+ @return: a list that contains the value of the <li> elements
+ """
+ rValue = []
+ if not listnode.nodeName in ["ul", "ol"]:
+ raise GlsaFormatException("Invalid function call: listnode is not <ul> or <ol>")
+ for li in listnode.childNodes:
+ if li.nodeType != xml.dom.Node.ELEMENT_NODE:
+ continue
+ rValue.append(getText(li, format="strip"))
+ return rValue
+
+def getText(node, format):
+ """
+ This is the main parser function. It takes a node and traverses
+ recursive over the subnodes, getting the text of each (and the
+ I{link} attribute for <uri> and <mail>). Depending on the I{format}
+ parameter the text might be formatted by adding/removing newlines,
+ tabs and spaces. This function is only useful for the GLSA DTD,
+ it's not applicable for other DTDs.
+
+ @type node: xml.dom.Node
+ @param node: the root node to start with the parsing
+ @type format: String
+ @param format: this should be either I{strip}, I{keep} or I{xml}
+ I{keep} just gets the text and does no formatting.
+ I{strip} replaces newlines and tabs with spaces and
+ replaces multiple spaces with one space.
+ I{xml} does some more formatting, depending on the
+ type of the encountered nodes.
+ @rtype: String
+ @return: the (formatted) content of the node and its subnodes
+ """
+ rValue = ""
+ if format in ["strip", "keep"]:
+ if node.nodeName in ["uri", "mail"]:
+ rValue += node.childNodes[0].data+": "+node.getAttribute("link")
+ else:
+ for subnode in node.childNodes:
+ if subnode.nodeName == "#text":
+ rValue += subnode.data
+ else:
+ rValue += getText(subnode, format)
+ else:
+ for subnode in node.childNodes:
+ if subnode.nodeName == "p":
+ for p_subnode in subnode.childNodes:
+ if p_subnode.nodeName == "#text":
+ rValue += p_subnode.data.strip()
+ elif p_subnode.nodeName in ["uri", "mail"]:
+ rValue += p_subnode.childNodes[0].data
+ rValue += " ( "+p_subnode.getAttribute("link")+" )"
+ rValue += NEWLINE_ESCAPE
+ elif subnode.nodeName == "ul":
+ for li in getListElements(subnode):
+ rValue += "-"+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" "
+ elif subnode.nodeName == "ol":
+ i = 0
+ for li in getListElements(subnode):
+ i = i+1
+ rValue += str(i)+"."+SPACE_ESCAPE+li+NEWLINE_ESCAPE+" "
+ elif subnode.nodeName == "code":
+ rValue += getText(subnode, format="keep").replace("\n", NEWLINE_ESCAPE)
+ if rValue[-1*len(NEWLINE_ESCAPE):] != NEWLINE_ESCAPE:
+ rValue += NEWLINE_ESCAPE
+ elif subnode.nodeName == "#text":
+ rValue += subnode.data
+ else:
+ raise GlsaFormatException("Invalid Tag found: ", subnode.nodeName)
+ if format == "strip":
+ rValue = rValue.strip(" \n\t")
+ rValue = re.sub("[\s]{2,}", " ", rValue)
+ # Hope that the utf conversion doesn't break anything else
+ return rValue.encode("utf_8")
+
+def getMultiTagsText(rootnode, tagname, format):
+ """
+ Returns a list with the text of all subnodes of type I{tagname}
+ under I{rootnode} (which itself is not parsed) using the given I{format}.
+
+ @type rootnode: xml.dom.Node
+ @param rootnode: the node to search for I{tagname}
+ @type tagname: String
+ @param tagname: the name of the tags to search for
+ @type format: String
+ @param format: see L{getText}
+ @rtype: List of Strings
+ @return: a list containing the text of all I{tagname} childnodes
+ """
+ rValue = []
+ for e in rootnode.getElementsByTagName(tagname):
+ rValue.append(getText(e, format))
+ return rValue
+
+def makeAtom(pkgname, versionNode):
+ """
+ creates from the given package name and information in the
+ I{versionNode} a (syntactical) valid portage atom.
+
+ @type pkgname: String
+ @param pkgname: the name of the package for this atom
+ @type versionNode: xml.dom.Node
+ @param versionNode: a <vulnerable> or <unaffected> Node that
+ contains the version information for this atom
+ @rtype: String
+ @return: the portage atom
+ """
+ rValue = opMapping[versionNode.getAttribute("range")] \
+ + pkgname \
+ + "-" + getText(versionNode, format="strip")
+ return str(rValue)
+
+def makeVersion(versionNode):
+ """
+ creates from the information in the I{versionNode} a
+ version string (format <op><version>).
+
+ @type versionNode: xml.dom.Node
+ @param versionNode: a <vulnerable> or <unaffected> Node that
+ contains the version information for this atom
+ @rtype: String
+ @return: the version string
+ """
+ return opMapping[versionNode.getAttribute("range")] \
+ +getText(versionNode, format="strip")
+
+def match(atom, portdbname, match_type="default"):
+ """
+ wrapper that calls revisionMatch() or portage.dbapi.match() depending on
+ the given atom.
+
+ @type atom: string
+ @param atom: a <~ or >~ atom or a normal portage atom that contains the atom to match against
+ @type portdb: portage.dbapi
+ @param portdb: one of the portage databases to use as information source
+ @type match_type: string
+ @param match_type: if != "default" passed as first argument to dbapi.xmatch
+ to apply the wanted visibility filters
+
+ @rtype: list of strings
+ @return: a list with the matching versions
+ """
+ db = portage.db["/"][portdbname].dbapi
+ if atom[2] == "~":
+ return revisionMatch(atom, db, match_type=match_type)
+ elif match_type == "default" or not hasattr(db, "xmatch"):
+ return db.match(atom)
+ else:
+ return db.xmatch(match_type, atom)
+
+def revisionMatch(revisionAtom, portdb, match_type="default"):
+ """
+ handler for the special >~, >=~, <=~ and <~ atoms that are supposed to behave
+ as > and < except that they are limited to the same version, the range only
+ applies to the revision part.
+
+ @type revisionAtom: string
+ @param revisionAtom: a <~ or >~ atom that contains the atom to match against
+ @type portdb: portage.dbapi
+ @param portdb: one of the portage databases to use as information source
+ @type match_type: string
+ @param match_type: if != "default" passed as first argument to portdb.xmatch
+ to apply the wanted visibility filters
+
+ @rtype: list of strings
+ @return: a list with the matching versions
+ """
+ if match_type == "default" or not hasattr(portdb, "xmatch"):
+ mylist = portdb.match(re.sub("-r[0-9]+$", "", revisionAtom[2:]))
+ else:
+ mylist = portdb.xmatch(match_type, re.sub("-r[0-9]+$", "", revisionAtom[2:]))
+ rValue = []
+ for v in mylist:
+ r1 = portage.pkgsplit(v)[-1][1:]
+ r2 = portage.pkgsplit(revisionAtom[3:])[-1][1:]
+ if eval(r1+" "+revisionAtom[0:2]+" "+r2):
+ rValue.append(v)
+ return rValue
+
+
+def getMinUpgrade(vulnerableList, unaffectedList, minimize=True):
+ """
+ Checks if the systemstate is matching an atom in
+ I{vulnerableList} and returns string describing
+ the lowest version for the package that matches an atom in
+ I{unaffectedList} and is greater than the currently installed
+ version or None if the system is not affected. Both
+ I{vulnerableList} and I{unaffectedList} should have the
+ same base package.
+
+ @type vulnerableList: List of Strings
+ @param vulnerableList: atoms matching vulnerable package versions
+ @type unaffectedList: List of Strings
+ @param unaffectedList: atoms matching unaffected package versions
+ @type minimize: Boolean
+ @param minimize: True for a least-change upgrade, False for emerge-like algorithm
+
+ @rtype: String | None
+ @return: the lowest unaffected version that is greater than
+ the installed version.
+ """
+ rValue = None
+ v_installed = []
+ u_installed = []
+ for v in vulnerableList:
+ v_installed += match(v, "vartree")
+
+ for u in unaffectedList:
+ u_installed += match(u, "vartree")
+
+ install_unaffected = True
+ for i in v_installed:
+ if i not in u_installed:
+ install_unaffected = False
+
+ if install_unaffected:
+ return rValue
+
+ for u in unaffectedList:
+ mylist = match(u, "porttree", match_type="match-all")
+ for c in mylist:
+ c_pv = portage.catpkgsplit(c)
+ i_pv = portage.catpkgsplit(portage.best(v_installed))
+ if portage.pkgcmp(c_pv[1:], i_pv[1:]) > 0 \
+ and (rValue == None \
+ or not match("="+rValue, "porttree") \
+ or (minimize ^ (portage.pkgcmp(c_pv[1:], portage.catpkgsplit(rValue)[1:]) > 0)) \
+ and match("="+c, "porttree")) \
+ and portage.db["/"]["porttree"].dbapi.aux_get(c, ["SLOT"]) == portage.db["/"]["vartree"].dbapi.aux_get(portage.best(v_installed), ["SLOT"]):
+ rValue = c_pv[0]+"/"+c_pv[1]+"-"+c_pv[2]
+ if c_pv[3] != "r0": # we don't like -r0 for display
+ rValue += "-"+c_pv[3]
+ return rValue
+
+
+# simple Exception classes to catch specific errors
+class GlsaTypeException(Exception):
+ def __init__(self, doctype):
+ Exception.__init__(self, "wrong DOCTYPE: %s" % doctype)
+
+class GlsaFormatException(Exception):
+ pass
+
+class GlsaArgumentException(Exception):
+ pass
+
+# GLSA xml data wrapper class
+class Glsa:
+ """
+ This class is a wrapper for the XML data and provides methods to access
+ and display the contained data.
+ """
+ def __init__(self, myid, myconfig):
+ """
+ Simple constructor to set the ID, store the config and gets the
+ XML data by calling C{self.read()}.
+
+ @type myid: String
+ @param myid: String describing the id for the GLSA object (standard
+ GLSAs have an ID of the form YYYYMM-nn) or an existing
+ filename containing a GLSA.
+ @type myconfig: portage.config
+ @param myconfig: the config that should be used for this object.
+ """
+ if re.match(r'\d{6}-\d{2}', myid):
+ self.type = "id"
+ elif os.path.exists(myid):
+ self.type = "file"
+ else:
+ raise GlsaArgumentException("Given ID "+myid+" isn't a valid GLSA ID or filename.")
+ self.nr = myid
+ self.config = myconfig
+ self.read()
+
+ def read(self):
+ """
+ Here we build the filename from the config and the ID and pass
+ it to urllib to fetch it from the filesystem or a remote server.
+
+ @rtype: None
+ @return: None
+ """
+ if self.config["CHECKMODE"] == "local":
+ repository = "file://" + self.config["GLSA_DIR"]
+ else:
+ repository = self.config["GLSA_SERVER"]
+ if self.type == "file":
+ myurl = "file://"+self.nr
+ else:
+ myurl = repository + self.config["GLSA_PREFIX"] + str(self.nr) + self.config["GLSA_SUFFIX"]
+ self.parse(urllib.urlopen(myurl))
+ return None
+
+ def parse(self, myfile):
+ """
+ This method parses the XML file and sets up the internal data
+ structures by calling the different helper functions in this
+ module.
+
+ @type myfile: String
+ @param myfile: Filename to grab the XML data from
+ @rtype: None
+ @returns: None
+ """
+ self.DOM = xml.dom.minidom.parse(myfile)
+ if not self.DOM.doctype:
+ raise GlsaTypeException(None)
+ elif self.DOM.doctype.systemId != "http://www.gentoo.org/dtd/glsa.dtd":
+ raise GlsaTypeException(self.DOM.doctype.systemId)
+ myroot = self.DOM.getElementsByTagName("glsa")[0]
+ if self.type == "id" and myroot.getAttribute("id") != self.nr:
+ raise GlsaFormatException("filename and internal id don't match:" + myroot.getAttribute("id") + " != " + self.nr)
+
+ # the simple (single, required, top-level, #PCDATA) tags first
+ self.title = getText(myroot.getElementsByTagName("title")[0], format="strip")
+ self.synopsis = getText(myroot.getElementsByTagName("synopsis")[0], format="strip")
+ self.announced = getText(myroot.getElementsByTagName("announced")[0], format="strip")
+ self.revised = getText(myroot.getElementsByTagName("revised")[0], format="strip")
+
+ # now the optional and 0-n toplevel, #PCDATA tags and references
+ try:
+ self.access = getText(myroot.getElementsByTagName("access")[0], format="strip")
+ except IndexError:
+ self.access = ""
+ self.bugs = getMultiTagsText(myroot, "bug", format="strip")
+ self.references = getMultiTagsText(myroot.getElementsByTagName("references")[0], "uri", format="keep")
+
+ # and now the formatted text elements
+ self.description = getText(myroot.getElementsByTagName("description")[0], format="xml")
+ self.workaround = getText(myroot.getElementsByTagName("workaround")[0], format="xml")
+ self.resolution = getText(myroot.getElementsByTagName("resolution")[0], format="xml")
+ self.impact_text = getText(myroot.getElementsByTagName("impact")[0], format="xml")
+ self.impact_type = myroot.getElementsByTagName("impact")[0].getAttribute("type")
+ try:
+ self.background = getText(myroot.getElementsByTagName("background")[0], format="xml")
+ except IndexError:
+ self.background = ""
+
+ # finally the interesting tags (product, affected, package)
+ self.glsatype = myroot.getElementsByTagName("product")[0].getAttribute("type")
+ self.product = getText(myroot.getElementsByTagName("product")[0], format="strip")
+ self.affected = myroot.getElementsByTagName("affected")[0]
+ self.packages = {}
+ for p in self.affected.getElementsByTagName("package"):
+ name = p.getAttribute("name")
+ if not self.packages.has_key(name):
+ self.packages[name] = []
+ tmp = {}
+ tmp["arch"] = p.getAttribute("arch")
+ tmp["auto"] = (p.getAttribute("auto") == "yes")
+ tmp["vul_vers"] = [makeVersion(v) for v in p.getElementsByTagName("vulnerable")]
+ tmp["unaff_vers"] = [makeVersion(v) for v in p.getElementsByTagName("unaffected")]
+ tmp["vul_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("vulnerable")]
+ tmp["unaff_atoms"] = [makeAtom(name, v) for v in p.getElementsByTagName("unaffected")]
+ self.packages[name].append(tmp)
+ # TODO: services aren't really used yet
+ self.services = self.affected.getElementsByTagName("service")
+ return None
+
+ def dump(self, outstream=sys.stdout):
+ """
+ Dumps a plaintext representation of this GLSA to I{outfile} or
+ B{stdout} if it is ommitted. You can specify an alternate
+ I{encoding} if needed (default is latin1).
+
+ @type outstream: File
+ @param outfile: Stream that should be used for writing
+ (defaults to sys.stdout)
+ """
+ width = int(self.config["PRINTWIDTH"])
+ outstream.write(center("GLSA %s: \n%s" % (self.nr, self.title), width)+"\n")
+ outstream.write((width*"=")+"\n")
+ outstream.write(wrap(self.synopsis, width, caption="Synopsis: ")+"\n")
+ outstream.write("Announced on: %s\n" % self.announced)
+ outstream.write("Last revised on: %s\n\n" % self.revised)
+ if self.glsatype == "ebuild":
+ for k in self.packages.keys():
+ pkg = self.packages[k]
+ for path in pkg:
+ vul_vers = "".join(path["vul_vers"])
+ unaff_vers = "".join(path["unaff_vers"])
+ outstream.write("Affected package: %s\n" % k)
+ outstream.write("Affected archs: ")
+ if path["arch"] == "*":
+ outstream.write("All\n")
+ else:
+ outstream.write("%s\n" % path["arch"])
+ outstream.write("Vulnerable: %s\n" % vul_vers)
+ outstream.write("Unaffected: %s\n\n" % unaff_vers)
+ elif self.glsatype == "infrastructure":
+ pass
+ if len(self.bugs) > 0:
+ outstream.write("\nRelated bugs: ")
+ for i in range(0, len(self.bugs)):
+ outstream.write(self.bugs[i])
+ if i < len(self.bugs)-1:
+ outstream.write(", ")
+ else:
+ outstream.write("\n")
+ if self.background:
+ outstream.write("\n"+wrap(self.background, width, caption="Background: "))
+ outstream.write("\n"+wrap(self.description, width, caption="Description: "))
+ outstream.write("\n"+wrap(self.impact_text, width, caption="Impact: "))
+ outstream.write("\n"+wrap(self.workaround, width, caption="Workaround: "))
+ outstream.write("\n"+wrap(self.resolution, width, caption="Resolution: "))
+ myreferences = ""
+ for r in self.references:
+ myreferences += (r.replace(" ", SPACE_ESCAPE)+NEWLINE_ESCAPE+" ")
+ outstream.write("\n"+wrap(myreferences, width, caption="References: "))
+ outstream.write("\n")
+
+ def isVulnerable(self):
+ """
+ Tests if the system is affected by this GLSA by checking if any
+ vulnerable package versions are installed. Also checks for affected
+ architectures.
+
+ @rtype: Boolean
+ @returns: True if the system is affected, False if not
+ """
+ vList = []
+ rValue = False
+ for k in self.packages.keys():
+ pkg = self.packages[k]
+ for path in pkg:
+ if path["arch"] == "*" or self.config["ARCH"] in path["arch"].split():
+ for v in path["vul_atoms"]:
+ rValue = rValue \
+ or (len(match(v, "vartree")) > 0 \
+ and getMinUpgrade(path["vul_atoms"], path["unaff_atoms"]))
+ return rValue
+
+ def isApplied(self):
+ """
+ Looks if the GLSA IDis in the GLSA checkfile to check if this
+ GLSA was already applied.
+
+ @rtype: Boolean
+ @returns: True if the GLSA was applied, False if not
+ """
+ aList = portage.grabfile(self.config["CHECKFILE"])
+ return (self.nr in aList)
+
+ def inject(self):
+ """
+ Puts the ID of this GLSA into the GLSA checkfile, so it won't
+ show up on future checks. Should be called after a GLSA is
+ applied or on explicit user request.
+
+ @rtype: None
+ @returns: None
+ """
+ if not self.isApplied():
+ checkfile = open(self.config["CHECKFILE"], "a+")
+ checkfile.write(self.nr+"\n")
+ checkfile.close()
+ return None
+
+ def getMergeList(self, least_change=True):
+ """
+ Returns the list of package-versions that have to be merged to
+ apply this GLSA properly. The versions are as low as possible
+ while avoiding downgrades (see L{getMinUpgrade}).
+
+ @type least_change: Boolean
+ @param least_change: True if the smallest possible upgrade should be selected,
+ False for an emerge-like algorithm
+ @rtype: List of Strings
+ @return: list of package-versions that have to be merged
+ """
+ rValue = []
+ for pkg in self.packages.keys():
+ for path in self.packages[pkg]:
+ update = getMinUpgrade(path["vul_atoms"], path["unaff_atoms"], minimize=least_change)
+ if update:
+ rValue.append(update)
+ return rValue
diff --git a/pym/gentoolkit/helpers.py b/pym/gentoolkit/helpers.py
new file mode 100644
index 0000000..69acc1d
--- /dev/null
+++ b/pym/gentoolkit/helpers.py
@@ -0,0 +1,162 @@
+#!/usr/bin/python2
+#
+# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+import portage
+from gentoolkit import *
+from package import *
+from pprinter import print_warn
+try:
+ from portage.util import unique_array
+except ImportError:
+ from portage_util import unique_array
+
+def find_packages(search_key, masked=False):
+ """Returns a list of Package objects that matched the search key."""
+ try:
+ if masked:
+ t = portage.db["/"]["porttree"].dbapi.xmatch("match-all", search_key)
+ t += portage.db["/"]["vartree"].dbapi.match(search_key)
+ else:
+ t = portage.db["/"]["porttree"].dbapi.match(search_key)
+ t += portage.db["/"]["vartree"].dbapi.match(search_key)
+ # catch the "amgigous package" Exception
+ except ValueError, e:
+ if isinstance(e[0],list):
+ t = []
+ for cp in e[0]:
+ if masked:
+ t += portage.db["/"]["porttree"].dbapi.xmatch("match-all", cp)
+ t += portage.db["/"]["vartree"].dbapi.match(cp)
+ else:
+ t += portage.db["/"]["porttree"].dbapi.match(cp)
+ t += portage.db["/"]["vartree"].dbapi.match(cp)
+ else:
+ raise ValueError(e)
+ except portage_exception.InvalidAtom, e:
+ print_warn("Invalid Atom: '%s'" % str(e))
+ return []
+ # Make the list of packages unique
+ t = unique_array(t)
+ t.sort()
+ return [Package(x) for x in t]
+
+def find_installed_packages(search_key, masked=False):
+ """Returns a list of Package objects that matched the search key."""
+ try:
+ t = portage.db["/"]["vartree"].dbapi.match(search_key)
+ # catch the "amgigous package" Exception
+ except ValueError, e:
+ if isinstance(e[0],list):
+ t = []
+ for cp in e[0]:
+ t += portage.db["/"]["vartree"].dbapi.match(cp)
+ else:
+ raise ValueError(e)
+ except portage_exception.InvalidAtom, e:
+ print_warn("Invalid Atom: '%s'" % str(e))
+ return []
+ return [Package(x) for x in t]
+
+def find_best_match(search_key):
+ """Returns a Package object for the best available candidate that
+ matched the search key."""
+ t = portage.db["/"]["porttree"].dep_bestmatch(search_key)
+ if t:
+ return Package(t)
+ return None
+
+def find_system_packages(prefilter=None):
+ """Returns a tuple of lists, first list is resolved system packages,
+ second is a list of unresolved packages."""
+ pkglist = settings.packages
+ resolved = []
+ unresolved = []
+ for x in pkglist:
+ cpv = x.strip()
+ if len(cpv) and cpv[0] == "*":
+ pkg = find_best_match(cpv)
+ if pkg:
+ resolved.append(pkg)
+ else:
+ unresolved.append(cpv)
+ return (resolved, unresolved)
+
+def find_world_packages(prefilter=None):
+ """Returns a tuple of lists, first list is resolved world packages,
+ seond is unresolved package names."""
+ f = open(portage.root+portage.WORLD_FILE)
+ pkglist = f.readlines()
+ resolved = []
+ unresolved = []
+ for x in pkglist:
+ cpv = x.strip()
+ if len(cpv) and cpv[0] != "#":
+ pkg = find_best_match(cpv)
+ if pkg:
+ resolved.append(pkg)
+ else:
+ unresolved.append(cpv)
+ return (resolved,unresolved)
+
+def find_all_installed_packages(prefilter=None):
+ """Returns a list of all installed packages, after applying the prefilter
+ function"""
+ t = vartree.dbapi.cpv_all()
+ if prefilter:
+ t = filter(prefilter,t)
+ return [Package(x) for x in t]
+
+def find_all_uninstalled_packages(prefilter=None):
+ """Returns a list of all uninstalled packages, after applying the prefilter
+ function"""
+ alist = find_all_packages(prefilter)
+ return [x for x in alist if not x.is_installed()]
+
+def find_all_packages(prefilter=None):
+ """Returns a list of all known packages, installed or not, after applying
+ the prefilter function"""
+ t = porttree.dbapi.cp_all()
+ t += vartree.dbapi.cp_all()
+ if prefilter:
+ t = filter(prefilter,t)
+ t = unique_array(t)
+ t2 = []
+ for x in t:
+ t2 += porttree.dbapi.cp_list(x)
+ t2 += vartree.dbapi.cp_list(x)
+ t2 = unique_array(t2)
+ return [Package(x) for x in t2]
+
+def split_package_name(name):
+ """Returns a list on the form [category, name, version, revision]. Revision will
+ be 'r0' if none can be inferred. Category and version will be empty, if none can
+ be inferred."""
+ r = portage.catpkgsplit(name)
+ if not r:
+ r = name.split("/")
+ if len(r) == 1:
+ return ["", name, "", "r0"]
+ else:
+ return r + ["", "r0"]
+ else:
+ r = list(r)
+ if r[0] == 'null':
+ r[0] = ''
+ return r
+
+def sort_package_list(pkglist):
+ """Returns the list ordered in the same way portage would do with lowest version
+ at the head of the list."""
+ pkglist.sort(Package.compare_version)
+ return pkglist
+
+if __name__ == "__main__":
+ print "This module is for import only"
+
+
diff --git a/pym/gentoolkit/helpers2.py b/pym/gentoolkit/helpers2.py
new file mode 100644
index 0000000..20d1de0
--- /dev/null
+++ b/pym/gentoolkit/helpers2.py
@@ -0,0 +1,425 @@
+# Copyright(c) 2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2 or higher
+
+"""Improved versions of the original helpers functions.
+
+As a convention, functions ending in '_packages' or '_match{es}' return
+Package objects, while functions ending in 'cpvs' return a sequence of strings.
+Functions starting with 'get_' return a set of packages by default and can be
+filtered, while functions starting with 'find_' return nothing unless the
+query matches one or more packages.
+
+This should be merged into helpers when a clean path is found.
+"""
+
+__all__ = (
+ 'compare_package_strings',
+ 'find_best_match',
+ 'find_installed_packages',
+ 'find_packages',
+ 'get_cpvs',
+ 'get_installed_cpvs',
+ 'get_uninstalled_cpvs',
+ 'uses_globbing',
+ 'do_lookup'
+)
+__author__ = 'Douglas Anderson'
+__docformat__ = 'epytext'
+
+# =======
+# Imports
+# =======
+
+import re
+import fnmatch
+from functools import partial
+
+import portage
+from portage.util import unique_array
+
+import gentoolkit
+import gentoolkit.pprinter as pp
+from gentoolkit import catpkgsplit, Config
+from gentoolkit import errors
+from gentoolkit.package import Package
+
+# =======
+# Globals
+# =======
+
+PORTDB = portage.db[portage.root]["porttree"].dbapi
+VARDB = portage.db[portage.root]["vartree"].dbapi
+
+# =========
+# Functions
+# =========
+
+def compare_package_strings(pkg1, pkg2):
+ """Similar to the builtin cmp, but for package strings. Usually called
+ as: package_list.sort(compare_package_strings)
+
+ An alternative is to use the Package descriptor from gentoolkit.package
+ >>> pkgs = [Package(x) for x in package_list]
+ >>> pkgs.sort()
+
+ @see: >>> help(cmp)
+ """
+
+ pkg1 = catpkgsplit(pkg1)
+ pkg2 = catpkgsplit(pkg2)
+ # Compare categories
+ if pkg1[0] != pkg2[0]:
+ return cmp(pkg1[0], pkg2[0])
+ # Compare names
+ elif pkg1[1] != pkg2[1]:
+ return cmp(pkg1[1], pkg2[1])
+ # Compare versions
+ else:
+ return portage.versions.pkgcmp(pkg1[1:], pkg2[1:])
+
+
+def find_best_match(query):
+ """Return the highest unmasked version of a package matching query.
+
+ @type query: str
+ @param query: can be of the form: pkg, pkg-ver, cat/pkg, cat/pkg-ver, atom
+ @rtype: str or None
+ """
+
+ match = PORTDB.xmatch("bestmatch-visible", query)
+
+ return Package(match) if match else None
+
+
+def find_installed_packages(query):
+ """Return a list of Package objects that matched the search key."""
+
+ try:
+ matches = VARDB.match(query)
+ # catch the ambiguous package Exception
+ except ValueError, err:
+ if isinstance(err[0], list):
+ matches = []
+ for pkgkey in err[0]:
+ matches.append(VARDB.match(pkgkey))
+ else:
+ raise ValueError(err)
+ except portage.exception.InvalidAtom, err:
+ pp.print_warn("Invalid Atom: '%s'" % str(err))
+ return []
+
+ return [Package(x) for x in matches]
+
+
+def uses_globbing(query):
+ """Check the query to see if it is using globbing.
+
+ @rtype: bool
+ @return: True if query uses globbing, else False
+ """
+
+ if set('!*?[]').intersection(set(query)):
+ if portage.dep.get_operator(query):
+ # Query may be an atom such as '=sys-apps/portage-2.2*'
+ pass
+ else:
+ return True
+
+ return False
+
+
+def _do_complex_lookup(query, query_opts):
+ """Find matches for a query which is a regex or includes globbing."""
+
+ result = []
+
+ if query_opts["includeInstalled"]:
+ if query_opts["includePortTree"] or query_opts["includeOverlayTree"]:
+ package_finder = get_cpvs
+ else:
+ package_finder = get_installed_cpvs
+ elif query_opts["includePortTree"] or query_opts["includeOverlayTree"]:
+ package_finder = get_uninstalled_cpvs
+ else:
+ pp.print_error("Not searching in installed, portage tree or overlay." +
+ " Nothing to do.")
+ pp.die(2, "This is an internal error. Please report this.")
+
+ if query_opts["printMatchInfo"] and not Config["piping"]:
+ print_query_info(query, query_opts)
+
+ cats = prepare_categories(query_opts["categoryFilter"])
+ cat = split_query(query)[0]
+
+ pre_filter = []
+ # The "get_" functions can pre-filter against the whole package key,
+ # but since we allow globbing now, we run into issues like:
+ # >>> portage.dep.dep_getkey("sys-apps/portage-*")
+ # 'sys-apps/portage-'
+ # So the only way to guarantee we don't overrun the key is to
+ # prefilter by cat only.
+ if cats:
+ pre_filter = package_finder(predicate=lambda x: x.startswith(cats))
+ if cat:
+ if query_opts["isRegex"]:
+ cat_re = cat
+ else:
+ cat_re = fnmatch.translate(cat)
+ # [::-1] reverses a sequence, so we're emulating an ".rreplace()"
+ # except we have to put our "new" string on backwards
+ cat_re = cat_re[::-1].replace('$', '*./', 1)[::-1]
+ predicate = lambda x: re.match(cat_re, x)
+ if pre_filter:
+ pre_filter = [x for x in pre_filter if predicate(x)]
+ else:
+ pre_filter = package_finder(predicate=predicate)
+
+ # Post-filter
+ if query_opts["isRegex"]:
+ predicate = lambda x: re.match(query, x)
+ else:
+ if cat:
+ query_re = fnmatch.translate(query)
+ else:
+ query_re = fnmatch.translate("*/%s" % query)
+ predicate = lambda x: re.search(query_re, x)
+ if pre_filter:
+ result = [x for x in pre_filter if predicate(x)]
+ else:
+ result = package_finder(predicate=predicate)
+
+ return [Package(x) for x in result]
+
+
+def print_query_info(query, query_opts):
+ """Print info about the query to the screen."""
+
+ cats = prepare_categories(query_opts["categoryFilter"])
+ cat, pkg, ver, rev = split_query(query)
+ del ver, rev
+ if cats:
+ cat_str = "in %s " % ', '.join([pp.emph(x) for x in cats])
+ elif cat and not query_opts["isRegex"]:
+ cat_str = "in %s " % pp.emph(cat)
+ else:
+ cat_str = ""
+
+ if query_opts["isRegex"]:
+ pkg_str = query
+ else:
+ pkg_str = pkg
+
+ print " * Searching for %s %s..." % (pp.emph(pkg_str), cat_str)
+
+
+def _do_simple_lookup(query, query_opts):
+ """Find matches for a query which is an atom or string."""
+
+ result = []
+
+ cats = prepare_categories(query_opts["categoryFilter"])
+ if query_opts["printMatchInfo"] and not Config["piping"]:
+ print_query_info(query, query_opts)
+
+ if query_opts["includePortTree"] or query_opts["includeOverlayTree"]:
+ package_finder = find_packages
+ else:
+ package_finder = find_installed_packages
+
+ result = package_finder(query)
+ if not query_opts["includeInstalled"]:
+ result = [x for x in result if not x.is_installed()]
+
+ if cats:
+ result = [x for x in result if x.cpv.startswith(cats)]
+
+ return result
+
+
+def do_lookup(query, query_opts):
+ """A high-level wrapper around gentoolkit package-finder functions.
+
+ @todo: equery modules to move to do_lookup: c,m,u,w
+
+ @type query: str
+ @param query: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom or regex
+ @type query_opts: dict
+ @param query_opts: user-configurable options from the calling module
+ Currently supported options are:
+
+ categoryFilter = str or None
+ includeInstalled = bool
+ includePortTree = bool
+ includeOverlayTree = bool
+ isRegex = bool
+ printMatchInfo = bool # Print info about the search
+
+ @rtype: list
+ @return: Package objects matching query
+ """
+
+ is_simple_query = True
+ if query_opts["isRegex"] or uses_globbing(query):
+ is_simple_query = False
+
+ if is_simple_query:
+ matches = _do_simple_lookup(query, query_opts)
+ else:
+ matches = _do_complex_lookup(query, query_opts)
+
+ return matches
+
+
+def find_packages(query, include_masked=False):
+ """Returns a list of Package objects that matched the query.
+
+ @type query: str
+ @param query: can be of the form: pkg, pkg-ver, cat/pkg, cat/pkg-ver, atom
+ @rtype: list
+ @return: matching Package objects
+ """
+
+ if not query:
+ return []
+
+ try:
+ if include_masked:
+ matches = PORTDB.xmatch("match-all", query)
+ else:
+ matches = PORTDB.match(query)
+ matches.extend(VARDB.match(query))
+ # Catch ambiguous packages
+ except ValueError, err:
+ if isinstance(err[0], list):
+ matches = []
+ for pkgkey in err[0]:
+ if include_masked:
+ matches.extend(PORTDB.xmatch("match-all", pkgkey))
+ else:
+ matches.extend(PORTDB.match(pkgkey))
+ matches.extend(VARDB.match(pkgkey))
+ else:
+ raise ValueError(err)
+ except portage.exception.InvalidAtom, err:
+ raise errors.GentoolkitInvalidAtom(str(err))
+
+ return [Package(x) for x in unique_array(matches)]
+
+
+def get_cpvs(predicate=None, include_installed=True):
+ """Get all packages in the Portage tree and overlays. Optionally apply a
+ predicate.
+
+ Example usage:
+ >>> from gentoolkit.helpers2 import get_cpvs
+ >>> len(get_cpvs())
+ 26065
+ >>> fn = lambda x: x.startswith('app-portage')
+ >>> len(get_cpvs(fn, include_installed=False))
+ 112
+
+ @type predicate: function
+ @param predicate: a function to filter the package list with
+ @type include_installed: bool
+ @param include_installed:
+ If True: Return the union of all_cpvs and all_installed_cpvs
+ If False: Return the difference of all_cpvs and all_installed_cpvs
+ @rtype: list
+ @return: ['cat/portdir_pkg-1', 'cat/overlay_pkg-2', ...]
+ """
+
+ if predicate:
+ all_cps = [x for x in PORTDB.cp_all() if predicate(x)]
+ else:
+ all_cps = PORTDB.cp_all()
+
+ all_cpvs = []
+ for pkgkey in all_cps:
+ all_cpvs.extend(PORTDB.cp_list(pkgkey))
+
+ result = set(all_cpvs)
+ all_installed_cpvs = get_installed_cpvs(predicate)
+
+ if include_installed:
+ result.update(all_installed_cpvs)
+ else:
+ result.difference_update(all_installed_cpvs)
+
+ return list(result)
+
+
+# pylint thinks this is a global variable
+# pylint: disable-msg=C0103
+get_uninstalled_cpvs = partial(get_cpvs, include_installed=False)
+
+
+def get_installed_cpvs(predicate=None):
+ """Get all installed packages. Optionally apply a predicate.
+
+ @type predicate: function
+ @param predicate: a function to filter the package list with
+ @rtype: unsorted list
+ @return: ['cat/installed_pkg-1', 'cat/installed_pkg-2', ...]
+ """
+
+ if predicate:
+ all_installed_cps = [x for x in VARDB.cp_all() if predicate(x)]
+ else:
+ all_installed_cps = VARDB.cp_all()
+
+ result = []
+ for pkgkey in all_installed_cps:
+ result.extend(VARDB.cp_list(pkgkey))
+
+ return list(result)
+
+
+def prepare_categories(category_filter):
+ """Return a tuple of validated categories. Expand globs.
+
+ Example usage:
+ >>> prepare_categories('app-portage,sys-apps')
+ ('app-portage', 'sys-apps')
+ """
+
+ if not category_filter:
+ return tuple()
+
+ cats = [x.lstrip('=') for x in category_filter.split(',')]
+ valid_cats = portage.settings.categories
+ good_cats = []
+ for cat in cats:
+ if set('!*?[]').intersection(set(cat)):
+ good_cats.extend(fnmatch.filter(valid_cats, cat))
+ elif cat in valid_cats:
+ good_cats.append(cat)
+ else:
+ raise errors.GentoolkitInvalidCategory(cat)
+
+ return tuple(good_cats)
+
+
+def split_query(query):
+ """Split a query, using either.
+
+ @see: split_atom, gentoolkit.split_package_name
+ @param query: pkg, cat/pkg, pkg-ver, cat/pkg-ver, atom or regex
+ @rtype: tuple
+ @return: (category, pkg_name, version, revision)
+ Each tuple element is a string or empty string ("").
+ """
+
+ cat = name = ver = rev = ""
+
+ try:
+ (cat, name, ver, rev) = gentoolkit.split_package_name(query)
+ except ValueError, err:
+ # FIXME: Not hitting this error anymore... but we should be?
+ if str(err) == 'too many values to unpack':
+ pp.print_error("Too many slashes ('/').")
+ raise errors.GentoolkitInvalidPackageName(query)
+ else:
+ raise ValueError(err)
+
+ return (cat, name, ver, rev)
diff --git a/pym/gentoolkit/package.py b/pym/gentoolkit/package.py
new file mode 100644
index 0000000..65cdb9a
--- /dev/null
+++ b/pym/gentoolkit/package.py
@@ -0,0 +1,582 @@
+#! /usr/bin/python
+#
+# Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright(c) 2004-2009, Gentoo Foundation
+#
+# Licensed under the GNU General Public License, v2
+#
+# $Header$
+
+# =======
+# Imports
+# =======
+
+import os
+
+import portage
+from portage import catpkgsplit
+from portage.versions import vercmp
+
+from gentoolkit import *
+from gentoolkit import errors
+
+# =======
+# Globals
+# =======
+
+PORTDB = portage.db[portage.root]["porttree"].dbapi
+VARDB = portage.db[portage.root]["vartree"].dbapi
+
+# =======
+# Classes
+# =======
+
+class Package(object):
+ """Package descriptor. Contains convenience functions for querying the
+ state of a package, its contents, name manipulation, ebuild info and
+ similar."""
+
+ def __init__(self, arg):
+
+ self._cpv = arg
+ self.cpv = self._cpv
+
+ if self.cpv[0] in ('<', '>'):
+ if self.cpv[1] == '=':
+ self.operator = self.cpv[:2]
+ self.cpv = self.cpv[2:]
+ else:
+ self.operator = self.cpv[0]
+ self.cpv = self.cpv[1:]
+ elif self.cpv[0] == '=':
+ if self.cpv[-1] == '*':
+ self.operator = '=*'
+ self.cpv = self.cpv[1:-1]
+ else:
+ self.cpv = self.cpv[1:]
+ self.operator = '='
+ elif self.cpv[0] == '~':
+ self.operator = '~'
+ self.cpv = self.cpv[1:]
+ else:
+ self.operator = '='
+ self._cpv = '=%s' % self._cpv
+
+ if not portage.dep.isvalidatom(self._cpv):
+ raise errors.GentoolkitInvalidCPV(self._cpv)
+
+ cpv_split = portage.catpkgsplit(self.cpv)
+
+ try:
+ self.key = "/".join(cpv_split[:2])
+ except TypeError:
+ # catpkgsplit returned None
+ raise errors.GentoolkitInvalidCPV(self._cpv)
+
+ cpv_split = list(cpv_split)
+ if cpv_split[0] == 'null':
+ cpv_split[0] = ''
+ if cpv_split[3] == 'r0':
+ cpv_split[3] = ''
+ self.cpv_split = cpv_split
+ self._scpv = self.cpv_split # XXX: namespace compatability 03/09
+
+ self._db = None
+ self._settings = settings
+ self._settingslock = settingslock
+ self._portdir_path = os.path.realpath(settings["PORTDIR"])
+
+ self.category = self.cpv_split[0]
+ self.name = self.cpv_split[1]
+ self.version = self.cpv_split[2]
+ self.revision = self.cpv_split[3]
+ if not self.revision:
+ self.fullversion = self.version
+ else:
+ self.fullversion = "%s-%s" % (self.version, self.revision)
+
+ def __repr__(self):
+ return "<%s %s @%#8x>" % (self.__class__.__name__, self._cpv, id(self))
+
+ def __cmp__(self, other):
+ # FIXME: __cmp__ functions dissallowed in py3k; need __lt__, __gt__.
+ if not isinstance(other, self.__class__):
+ raise TypeError("other isn't of %s type, is %s" %
+ (self.__class__, other.__class__))
+
+ if self.category != other.category:
+ return cmp(self.category, other.category)
+ elif self.name != other.name:
+ return cmp(self.name, other.name)
+ else:
+ # FIXME: this cmp() hack is for vercmp not using -1,0,1
+ # See bug 266493; this was fixed in portage-2.2_rc31
+ #return portage.vercmp(self.fullversion, other.fullversion)
+ return cmp(portage.vercmp(self.fullversion, other.fullversion), 0)
+
+ def __eq__(self, other):
+ return hash(self) == hash(other)
+
+ def __ne__(self, other):
+ return hash(self) != hash(other)
+
+ def __hash__(self):
+ return hash(self._cpv)
+
+ def __contains__(self, key):
+ return key in self._cpv
+
+ def __str__(self):
+ return self._cpv
+
+ def get_name(self):
+ """Returns base name of package, no category nor version"""
+ return self.name
+
+ def get_version(self):
+ """Returns version of package, with revision number"""
+ return self.fullversion
+
+ def get_category(self):
+ """Returns category of package"""
+ return self.category
+
+ def get_settings(self, key):
+ """Returns the value of the given key for this package (useful
+ for package.* files."""
+ self._settingslock.acquire()
+ self._settings.setcpv(self.cpv)
+ v = self._settings[key]
+ self._settingslock.release()
+ return v
+
+ def get_cpv(self):
+ """Returns full Category/Package-Version string"""
+ return self.cpv
+
+ def get_provide(self):
+ """Return a list of provides, if any"""
+ if not self.is_installed():
+ try:
+ x = [self.get_env_var('PROVIDE')]
+ except KeyError:
+ x = []
+ return x
+ else:
+ return vartree.get_provide(self.cpv)
+
+ def get_dependants(self):
+ """Retrieves a list of CPVs for all packages depending on this one"""
+ raise NotImplementedError("Not implemented yet!")
+
+ def get_runtime_deps(self):
+ """Returns a linearised list of first-level run time dependencies for
+ this package, on the form [(comparator, [use flags], cpv), ...]
+ """
+ # Try to use the portage tree first, since emerge only uses the tree
+ # when calculating dependencies
+ try:
+ cd = self.get_env_var("RDEPEND", porttree).split()
+ except KeyError:
+ cd = self.get_env_var("RDEPEND", vartree).split()
+ r,i = self._parse_deps(cd)
+ return r
+
+ def get_compiletime_deps(self):
+ """Returns a linearised list of first-level compile time dependencies
+ for this package, on the form [(comparator, [use flags], cpv), ...]
+ """
+ # Try to use the portage tree first, since emerge only uses the tree
+ # when calculating dependencies
+ try:
+ rd = self.get_env_var("DEPEND", porttree).split()
+ except KeyError:
+ rd = self.get_env_var("DEPEND", vartree).split()
+ r,i = self._parse_deps(rd)
+ return r
+
+ def get_postmerge_deps(self):
+ """Returns a linearised list of first-level post merge dependencies
+ for this package, on the form [(comparator, [use flags], cpv), ...]
+ """
+ # Try to use the portage tree first, since emerge only uses the tree
+ # when calculating dependencies
+ try:
+ pd = self.get_env_var("PDEPEND", porttree).split()
+ except KeyError:
+ pd = self.get_env_var("PDEPEND", vartree).split()
+ r,i = self._parse_deps(pd)
+ return r
+
+ def intersects(self, other):
+ """Check if a passed in package atom "intersects" this atom.
+
+ Lifted from pkgcore.
+
+ Two atoms "intersect" if a package can be constructed that
+ matches both:
+ - if you query for just "dev-lang/python" it "intersects" both
+ "dev-lang/python" and ">=dev-lang/python-2.4"
+ - if you query for "=dev-lang/python-2.4" it "intersects"
+ ">=dev-lang/python-2.4" and "dev-lang/python" but not
+ "<dev-lang/python-2.3"
+
+ @type other: gentoolkit.package.Package
+ @param other: other package to compare
+ @see: pkgcore.ebuild.atom.py
+ """
+ # Our "key" (cat/pkg) must match exactly:
+ if self.key != other.key:
+ return False
+
+ # If we are both "unbounded" in the same direction we intersect:
+ if (('<' in self.operator and '<' in other.operator) or
+ ('>' in self.operator and '>' in other.operator)):
+ return True
+
+ # If one of us is an exact match we intersect if the other matches it:
+ if self.operator == '=':
+ if other.operator == '=*':
+ return self.fullversion.startswith(other.fullversion)
+ return VersionMatch(frompkg=other).match(self)
+ if other.operator == '=':
+ if self.operator == '=*':
+ return other.fullversion.startswith(self.fullversion)
+ return VersionMatch(frompkg=self).match(other)
+
+ # If we are both ~ matches we match if we are identical:
+ if self.operator == other.operator == '~':
+ return (self.version == other.version and
+ self.revision == other.revision)
+
+ # If we are both glob matches we match if one of us matches the other.
+ if self.operator == other.operator == '=*':
+ return (self.fullver.startswith(other.fullver) or
+ other.fullver.startswith(self.fullver))
+
+ # If one of us is a glob match and the other a ~ we match if the glob
+ # matches the ~ (ignoring a revision on the glob):
+ if self.operator == '=*' and other.operator == '~':
+ return other.fullversion.startswith(self.version)
+ if other.operator == '=*' and self.operator == '~':
+ return self.fullversion.startswith(other.version)
+
+ # If we get here at least one of us is a <, <=, > or >=:
+ if self.operator in ('<', '<=', '>', '>='):
+ ranged, other = self, other
+ else:
+ ranged, other = other, self
+
+ if '<' in other.operator or '>' in other.operator:
+ # We are both ranged, and in the opposite "direction" (or
+ # we would have matched above). We intersect if we both
+ # match the other's endpoint (just checking one endpoint
+ # is not enough, it would give a false positive on <=2 vs >2)
+ return (
+ VersionMatch(frompkg=other).match(ranged) and
+ VersionMatch(frompkg=ranged).match(other))
+
+ if other.operator == '~':
+ # Other definitely matches its own version. If ranged also
+ # does we're done:
+ if VersionMatch(frompkg=ranged).match(other):
+ return True
+ # The only other case where we intersect is if ranged is a
+ # > or >= on other's version and a nonzero revision. In
+ # that case other will match ranged. Be careful not to
+ # give a false positive for ~2 vs <2 here:
+ return ranged.operator in ('>', '>=') and VersionMatch(
+ other.operator, other.version, other.revision).match(ranged)
+
+ if other.operator == '=*':
+ # a glob match definitely matches its own version, so if
+ # ranged does too we're done:
+ if VersionMatch(
+ ranged.operator, ranged.version, ranged.revision).match(other):
+ return True
+ if '<' in ranged.operator:
+ # If other.revision is not defined then other does not
+ # match anything smaller than its own fullver:
+ if not other.revision:
+ return False
+
+ # If other.revision is defined then we can always
+ # construct a package smaller than other.fullver by
+ # tagging e.g. an _alpha1 on.
+ return ranged.fullversion.startswith(other.version)
+ else:
+ # Remaining cases where this intersects: there is a
+ # package greater than ranged.fullver and
+ # other.fullver that they both match.
+ return ranged.fullversion.startswith(other.version)
+
+ # Handled all possible ops.
+ raise NotImplementedError(
+ 'Someone added an operator without adding it to intersects')
+
+
+ def _parse_deps(self,deps,curuse=[],level=0):
+ # store (comparator, [use predicates], cpv)
+ r = []
+ comparators = ["~","<",">","=","<=",">="]
+ end = len(deps)
+ i = 0
+ while i < end:
+ tok = deps[i]
+ if tok == ')':
+ return r,i
+ if tok[-1] == "?":
+ tok = tok.replace("?","")
+ sr,l = self._parse_deps(deps[i+2:],curuse=curuse+[tok],level=level+1)
+ r += sr
+ i += l + 3
+ continue
+ if tok == "||":
+ sr,l = self._parse_deps(deps[i+2:],curuse,level=level+1)
+ r += sr
+ i += l + 3
+ continue
+ # conjunction, like in "|| ( ( foo bar ) baz )" => recurse
+ if tok == "(":
+ sr,l = self._parse_deps(deps[i+1:],curuse,level=level+1)
+ r += sr
+ i += l + 2
+ continue
+ # pkg block "!foo/bar" => ignore it
+ if tok[0] == "!":
+ i += 1
+ continue
+ # pick out comparator, if any
+ cmp = ""
+ for c in comparators:
+ if tok.find(c) == 0:
+ cmp = c
+ tok = tok[len(cmp):]
+ r.append((cmp,curuse,tok))
+ i += 1
+ return r,i
+
+ def is_installed(self):
+ """Returns True if this package is installed (merged)"""
+ return VARDB.cpv_exists(self.cpv)
+
+ def is_overlay(self):
+ """Returns True if the package is in an overlay."""
+ dir,ovl = portage.portdb.findname2(self.cpv)
+ return ovl != self._portdir_path
+
+ def is_masked(self):
+ """Returns true if this package is masked against installation.
+ Note: We blindly assume that the package actually exists on disk
+ somewhere."""
+ unmasked = portage.portdb.xmatch("match-visible", self.cpv)
+ return self.cpv not in unmasked
+
+ def get_ebuild_path(self,in_vartree=0):
+ """Returns the complete path to the .ebuild file"""
+ if in_vartree:
+ return vartree.getebuildpath(self.cpv)
+ else:
+ return portage.portdb.findname(self.cpv)
+
+ def get_package_path(self):
+ """Returns the path to where the ChangeLog, Manifest, .ebuild files
+ reside"""
+ p = self.get_ebuild_path()
+ sp = p.split("/")
+ if sp:
+ # FIXME: use os.path.join
+ return "/".join(sp[:-1])
+
+ def get_env_var(self, var, tree=""):
+ """Returns one of the predefined env vars DEPEND, RDEPEND,
+ SRC_URI,...."""
+ if tree == "":
+ mytree = vartree
+ if not self.is_installed():
+ mytree = porttree
+ else:
+ mytree = tree
+ try:
+ r = mytree.dbapi.aux_get(self.cpv,[var])
+ except KeyError:
+ # aux_get raises KeyError if it encounters a bad digest, etc
+ raise
+ if not r:
+ raise errors.GentoolkitFatalError("Could not find the package tree")
+ if len(r) != 1:
+ raise errors.GentoolkitFatalError("Should only get one element!")
+ return r[0]
+
+ def get_use_flags(self):
+ """Returns the USE flags active at time of installation"""
+ self._initdb()
+ if self.is_installed():
+ return self._db.getfile("USE")
+ return ""
+
+ def get_contents(self):
+ """Returns the full contents, as a dictionary, in the form
+ [ '/bin/foo' : [ 'obj', '1052505381', '45ca8b89751...' ], ... ]"""
+ self._initdb()
+ if self.is_installed():
+ return self._db.getcontents()
+ return {}
+
+ # XXX >
+ def compare_version(self,other):
+ """Compares this package's version to another's CPV; returns -1, 0, 1.
+
+ Deprecated in favor of __cmp__.
+ """
+ v1 = self.cpv_split
+ v2 = catpkgsplit(other.get_cpv())
+ # if category is different
+ if v1[0] != v2[0]:
+ return cmp(v1[0],v2[0])
+ # if name is different
+ elif v1[1] != v2[1]:
+ return cmp(v1[1],v2[1])
+ # Compare versions
+ else:
+ return portage.pkgcmp(v1[1:],v2[1:])
+ # < XXX
+
+ def size(self):
+ """Estimates the installed size of the contents of this package,
+ if possible.
+ Returns [size, number of files in total, number of uncounted files]
+ """
+ contents = self.get_contents()
+ size = 0
+ uncounted = 0
+ files = 0
+ for x in contents:
+ try:
+ size += os.lstat(x).st_size
+ files += 1
+ except OSError:
+ uncounted += 1
+ return [size, files, uncounted]
+
+ def _initdb(self):
+ """Internal helper function; loads package information from disk,
+ when necessary.
+ """
+ if not self._db:
+ self._db = portage.dblink(
+ category,
+ "%s-%s" % (self.name, self.fullversion),
+ settings["ROOT"],
+ settings
+ )
+
+
+class VersionMatch(object):
+ """Package restriction implementing Gentoo ebuild version comparison rules.
+ From pkgcore.ebuild.atom_restricts.
+
+ Any overriding of this class *must* maintain numerical order of
+ self.vals, see intersect for reason why. vals also must be a tuple.
+ """
+ _convert_op2int = {(-1,):"<", (-1, 0): "<=", (0,):"=",
+ (0, 1):">=", (1,):">"}
+
+ _convert_int2op = dict([(v, k) for k, v in _convert_op2int.iteritems()])
+ del k, v
+
+ def __init__(self, **kwargs):
+ """This class will either create a VersionMatch instance out of
+ a Package instance, or from explicitly passed in operator, version,
+ and revision.
+
+ Possible args:
+ frompkg=<gentoolkit.package.Package> instance
+
+ OR
+
+ op=str: version comparison to do,
+ valid operators are ('<', '<=', '=', '>=', '>', '~')
+ ver=str: version to base comparison on
+ rev=str: revision to base comparison on
+ """
+ if 'frompkg' in kwargs and kwargs['frompkg']:
+ self.operator = kwargs['frompkg'].operator
+ self.version = kwargs['frompkg'].version
+ self.revision = kwargs['frompkg'].revision
+ self.fullversion = kwargs['frompkg'].fullversion
+ elif set(('op', 'ver', 'rev')) == set(kwargs):
+ self.operator = kwargs['op']
+ self.version = kwargs['ver']
+ self.revision = kwargs['rev']
+ if not self.revision:
+ self.fullversion = self.version
+ else:
+ self.fullversion = "%s-%s" % (self.version, self.revision)
+ else:
+ raise TypeError('__init__() takes either a Package instance '
+ 'via frompkg= or op=, ver= and rev= all passed in')
+
+ if self.operator != "~" and self.operator not in self._convert_int2op:
+ # FIXME: change error
+ raise errors.InvalidVersion(self.ver, self.rev,
+ "invalid operator, '%s'" % operator)
+
+ if self.operator == "~":
+ if not self.version:
+ raise ValueError(
+ "for ~ op, version must be specified")
+ self.droprevision = True
+ self.values = (0,)
+ else:
+ self.droprevision = False
+ self.values = self._convert_int2op[self.operator]
+
+ def match(self, pkginst):
+ if self.droprevision:
+ ver1, ver2 = self.version, pkginst.version
+ else:
+ ver1, ver2 = self.fullversion, pkginst.fullversion
+
+ #print "== VersionMatch.match DEBUG START =="
+ #print "ver1:", ver1
+ #print "ver2:", ver2
+ #print "vercmp(ver2, ver1):", vercmp(ver2, ver1)
+ #print "self.values:", self.values
+ #print "vercmp(ver2, ver1) in values?",
+ #print "vercmp(ver2, ver1) in self.values"
+ #print "== VersionMatch.match DEBUG END =="
+
+ return vercmp(ver2, ver1) in self.values
+
+ def __str__(self):
+ s = self._convert_op2int[self.values]
+
+ if self.droprevision or not self.revision:
+ return "ver %s %s" % (s, self.version)
+ return "ver-rev %s %s-%s" % (s, self.version, self.revision)
+
+ def __repr__(self):
+ return "<%s %s @%#8x>" % (self.__class__.__name__, str(self), id(self))
+
+ @staticmethod
+ def _convert_ops(inst):
+ if inst.droprevision:
+ return inst.values
+ return tuple(sorted(set((-1, 0, 1)).difference(inst.values)))
+
+ def __eq__(self, other):
+ if self is other:
+ return True
+ if isinstance(other, self.__class__):
+ if (self.droprevsion != other.droprevsion or
+ self.version != other.version or
+ self.revision != other.revision):
+ return False
+ return self._convert_ops(self) == self._convert_ops(other)
+
+ return False
+
+ def __hash__(self):
+ return hash((self.droprevision, self.version, self.revision,
+ self.values))
diff --git a/pym/gentoolkit/pprinter.py b/pym/gentoolkit/pprinter.py
new file mode 100644
index 0000000..ff92a26
--- /dev/null
+++ b/pym/gentoolkit/pprinter.py
@@ -0,0 +1,116 @@
+#!/usr/bin/python
+#
+# Copyright 2004 Karl Trygve Kalleberg <karltk@gentoo.org>
+# Copyright 2004 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+#
+# $Header$
+
+import sys
+import gentoolkit
+
+try:
+ import portage.output as output
+except ImportError:
+ import output
+
+
+def print_error(s):
+ """Prints an error string to stderr."""
+ sys.stderr.write(output.red("!!! ") + s + "\n")
+
+def print_info(lv, s, line_break = True):
+ """Prints an informational string to stdout."""
+ if gentoolkit.Config["verbosityLevel"] >= lv:
+ sys.stdout.write(s)
+ if line_break:
+ sys.stdout.write("\n")
+
+def print_warn(s):
+ """Print a warning string to stderr."""
+ sys.stderr.write("!!! " + s + "\n")
+
+def die(err, s):
+ """Print an error string and die with an error code."""
+ print_error(s)
+ sys.exit(err)
+
+# Colour settings
+
+def cpv(s):
+ """Print a category/package-<version> string."""
+ return output.green(s)
+
+def slot(s):
+ """Print a slot string"""
+ return output.bold(s)
+
+def useflag(s):
+ """Print a USE flag strign"""
+ return output.blue(s)
+
+def useflagon(s):
+ """Print an enabled USE flag string"""
+ # FIXME: Collapse into useflag with parameter
+ return output.red(s)
+
+def useflagoff(s):
+ """Print a disabled USE flag string"""
+ # FIXME: Collapse into useflag with parameter
+ return output.blue(s)
+
+def maskflag(s):
+ """Print a masking flag string"""
+ return output.red(s)
+
+def installedflag(s):
+ """Print an installed flag string"""
+ return output.bold(s)
+
+def number(s):
+ """Print a number string"""
+ return output.turquoise(s)
+
+def pkgquery(s):
+ """Print a package query string."""
+ return output.bold(s)
+
+def regexpquery(s):
+ """Print a regular expression string"""
+ return output.bold(s)
+
+def path(s):
+ """Print a file or directory path string"""
+ return output.bold(s)
+
+def path_symlink(s):
+ """Print a symlink string."""
+ return output.turquoise(s)
+
+def productname(s):
+ """Print a product name string, i.e. the program name."""
+ return output.turquoise(s)
+
+def globaloption(s):
+ """Print a global option string, i.e. the program global options."""
+ return output.yellow(s)
+
+def localoption(s):
+ """Print a local option string, i.e. the program local options."""
+ return output.green(s)
+
+def command(s):
+ """Print a program command string."""
+ return output.green(s)
+
+def section(s):
+ """Print a string as a section header."""
+ return output.turquoise(s)
+
+def subsection(s):
+ """Print a string as a subsection header."""
+ return output.turquoise(s)
+
+def emph(s):
+ """Print a string as emphasized."""
+ return output.bold(s)
diff --git a/pym/gentoolkit/tests/equery/test_init.py b/pym/gentoolkit/tests/equery/test_init.py
new file mode 100644
index 0000000..9756aba
--- /dev/null
+++ b/pym/gentoolkit/tests/equery/test_init.py
@@ -0,0 +1,43 @@
+import unittest
+from test import test_support
+
+from gentoolkit import equery
+
+class TestEqueryInit(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_expand_module_name(self):
+ # Test that module names are properly expanded
+ name_map = {
+ 'b': 'belongs',
+ 'c': 'changes',
+ 'k': 'check',
+ 'd': 'depends',
+ 'g': 'depgraph',
+ 'f': 'files',
+ 'h': 'hasuse',
+ 'l': 'list_',
+ 'm': 'meta',
+ 's': 'size',
+ 'u': 'uses',
+ 'w': 'which'
+ }
+ for short_name, long_name in zip(name_map, name_map.values()):
+ self.failUnlessEqual(equery.expand_module_name(short_name),
+ long_name)
+ self.failUnlessEqual(equery.expand_module_name(long_name),
+ long_name)
+ unused_keys = set(map(chr, range(0, 256))).difference(name_map.keys())
+ for key in unused_keys:
+ self.failUnlessRaises(KeyError, equery.expand_module_name, key)
+
+def test_main():
+ test_support.run_unittest(TestEqueryInit)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/pym/gentoolkit/tests/test_helpers2.py b/pym/gentoolkit/tests/test_helpers2.py
new file mode 100644
index 0000000..615cfa1
--- /dev/null
+++ b/pym/gentoolkit/tests/test_helpers2.py
@@ -0,0 +1,39 @@
+import unittest
+from test import test_support
+
+from gentoolkit import helpers2
+
+class TestGentoolkitHelpers2(unittest.TestCase):
+
+ def test_compare_package_strings(self):
+ # Test ordering of package strings, Portage has test for vercmp,
+ # so just do the rest
+ version_tests = [
+ # different categories
+ ('sys-apps/portage-2.1.6.8', 'sys-auth/pambase-20080318'),
+ # different package names
+ ('sys-apps/pkgcore-0.4.7.15-r1', 'sys-apps/portage-2.1.6.8'),
+ # different package versions
+ ('sys-apps/portage-2.1.6.8', 'sys-apps/portage-2.2_rc25')
+ ]
+ # Check less than
+ for vt in version_tests:
+ self.failUnless(
+ helpers2.compare_package_strings(vt[0], vt[1]) == -1
+ )
+ # Check greater than
+ for vt in version_tests:
+ self.failUnless(
+ helpers2.compare_package_strings(vt[1], vt[0]) == 1
+ )
+ # Check equal
+ vt = ('sys-auth/pambase-20080318', 'sys-auth/pambase-20080318')
+ self.failUnless(
+ helpers2.compare_package_strings(vt[0], vt[1]) == 0
+ )
+
+def test_main():
+ test_support.run_unittest(TestGentoolkitHelpers2)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/pym/gentoolkit/tests/test_template.py b/pym/gentoolkit/tests/test_template.py
new file mode 100644
index 0000000..84e8432
--- /dev/null
+++ b/pym/gentoolkit/tests/test_template.py
@@ -0,0 +1,38 @@
+import unittest
+from test import test_support
+
+class MyTestCase1(unittest.TestCase):
+
+ # Only use setUp() and tearDown() if necessary
+
+ def setUp(self):
+ ... code to execute in preparation for tests ...
+
+ def tearDown(self):
+ ... code to execute to clean up after tests ...
+
+ def test_feature_one(self):
+ # Test feature one.
+ ... testing code ...
+
+ def test_feature_two(self):
+ # Test feature two.
+ ... testing code ...
+
+ ... more test methods ...
+
+class MyTestCase2(unittest.TestCase):
+ ... same structure as MyTestCase1 ...
+
+... more test classes ...
+
+def test_main():
+ test_support.run_unittest(
+ MyTestCase1,
+ MyTestCase2,
+ ... list other tests ...
+ )
+
+if __name__ == '__main__':
+ test_main()
+
diff --git a/pym/gentoolkit/textwrap_.py b/pym/gentoolkit/textwrap_.py
new file mode 100644
index 0000000..6851402
--- /dev/null
+++ b/pym/gentoolkit/textwrap_.py
@@ -0,0 +1,97 @@
+"""This modification of textwrap allows it to wrap ANSI colorized text as if
+it weren't colorized. It also uses a much simpler word splitting regex to
+prevent the splitting of ANSI colors as well as package names and versions."""
+
+import re
+import textwrap
+
+class TextWrapper(textwrap.TextWrapper):
+ """Ignore ANSI escape codes while wrapping text"""
+
+ def _split(self, text):
+ """_split(text : string) -> [string]
+
+ Split the text to wrap into indivisible chunks.
+ """
+ # Only split on whitespace to avoid mangling ANSI escape codes or
+ # package names.
+ wordsep_re = re.compile(r'(\s+)')
+ chunks = wordsep_re.split(text)
+ chunks = filter(None, chunks)
+ return chunks
+
+ def _wrap_chunks(self, chunks):
+ """_wrap_chunks(chunks : [string]) -> [string]
+
+ Wrap a sequence of text chunks and return a list of lines of
+ length 'self.width' or less. (If 'break_long_words' is false,
+ some lines may be longer than this.) Chunks correspond roughly
+ to words and the whitespace between them: each chunk is
+ indivisible (modulo 'break_long_words'), but a line break can
+ come between any two chunks. Chunks should not have internal
+ whitespace; ie. a chunk is either all whitespace or a "word".
+ Whitespace chunks will be removed from the beginning and end of
+ lines, but apart from that whitespace is preserved.
+ """
+ lines = []
+ if self.width <= 0:
+ raise ValueError("invalid width %r (must be > 0)" % self.width)
+
+ # Arrange in reverse order so items can be efficiently popped
+ # from a stack of chunks.
+ chunks.reverse()
+
+ # Regex to strip ANSI escape codes. It's only used for the
+ # length calculations of indent and each chuck.
+ ansi_re = re.compile('\x1b\[[0-9;]*m')
+
+ while chunks:
+
+ # Start the list of chunks that will make up the current line.
+ # cur_len is just the length of all the chunks in cur_line.
+ cur_line = []
+ cur_len = 0
+
+ # Figure out which static string will prefix this line.
+ if lines:
+ indent = self.subsequent_indent
+ else:
+ indent = self.initial_indent
+
+ # Maximum width for this line. Ingore ANSI escape codes.
+ width = self.width - len(re.sub(ansi_re, '', indent))
+
+ # First chunk on line is whitespace -- drop it, unless this
+ # is the very beginning of the text (ie. no lines started yet).
+ if chunks[-1].strip() == '' and lines:
+ del chunks[-1]
+
+ while chunks:
+ # Ignore ANSI escape codes.
+ l = len(re.sub(ansi_re, '', chunks[-1]))
+
+ # Can at least squeeze this chunk onto the current line.
+ if cur_len + l <= width:
+ cur_line.append(chunks.pop())
+ cur_len += l
+
+ # Nope, this line is full.
+ else:
+ break
+
+ # The current line is full, and the next chunk is too big to
+ # fit on *any* line (not just this one).
+ # Ignore ANSI escape codes.
+ if chunks and len(re.sub(ansi_re, '', chunks[-1])) > width:
+ self._handle_long_word(chunks, cur_line, cur_len, width)
+
+ # If the last chunk on this line is all whitespace, drop it.
+ if cur_line and cur_line[-1].strip() == '':
+ del cur_line[-1]
+
+ # Convert current line back to a string and store it in list
+ # of all lines (return value).
+ if cur_line:
+ lines.append(indent + ''.join(cur_line))
+
+ return lines