diff options
Diffstat (limited to 'pym')
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 |
