[yum-commits] docs/show-installed.1 docs/yum-utils.1 Makefile repoclosure.py show-installed.py yum-utils.spec

Florian Festi ffesti at osuosl.org
Tue Oct 26 16:15:02 UTC 2010


 Makefile              |    2 
 docs/show-installed.1 |   52 ++++++
 docs/yum-utils.1      |    1 
 repoclosure.py        |    4 
 show-installed.py     |  409 ++++++++++++++++++++++++++++++++++++++++++++++++++
 yum-utils.spec        |    1 
 6 files changed, 466 insertions(+), 3 deletions(-)

New commits:
commit 9b1900cfa52a2f4cce104dfd3c96f1e83ae5151c
Author: Florian Festi <ffesti at redhat.com>
Date:   Wed Oct 6 13:32:46 2010 +0200

    show-installed, a new tool to give a compat description of the packages installed on the system making use of dependencies and comps groups

diff --git a/Makefile b/Makefile
index 79d1e5e..bea99d7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 SUBDIRS = docs
 PKGNAME = yum-utils
-UTILS = package-cleanup debuginfo-install repoclosure repomanage repoquery repo-graph repo-rss yumdownloader yum-builddep repotrack reposync repodiff yum-debug-dump yum-debug-restore verifytree yum-groups-manager find-repos-of-install needs-restarting yum-config-manager
+UTILS = package-cleanup debuginfo-install repoclosure repomanage repoquery repo-graph repo-rss yumdownloader yum-builddep repotrack reposync repodiff yum-debug-dump yum-debug-restore verifytree yum-groups-manager find-repos-of-install needs-restarting yum-config-manager show-installed
 UTILSROOT = yum-complete-transaction yumdb
 VERSION=$(shell awk '/Version:/ { print $$2 }' ${PKGNAME}.spec)
 RELEASE=$(shell awk -F%: '/Release:/ { print $$2 }' ${PKGNAME}.spec ')
diff --git a/docs/show-installed.1 b/docs/show-installed.1
new file mode 100644
index 0000000..4ce248b
--- /dev/null
+++ b/docs/show-installed.1
@@ -0,0 +1,52 @@
+.\" show-installed
+.TH "show-installed" "1" "21 October 2010" "Florian Festi" ""
+.SH "NAME"
+show\-installed
+.SH "SYNOPSIS"
+\fBshow\-installed\fP [options]
+.SH "DESCRIPTION"
+.PP
+\fBshow\-installed\fP gives a compact description of the packages installed (or given) making use of the comps groups found in the repositories.
+.SH OPTIONS
+.TP
+.B \-h, \-\-help
+show this help message and exit
+.TP
+.B \-f FORMAT, \-\-format=FORMAT
+yum, kickstart or human; yum gives the result as a yum command line; kickstart the content of a %packages section; "human" readable is default.
+.TP
+.B \-i INPUT, \-\-input=INPUT
+File to read the package list from instead of using the rpmdb. \- for stdin. The file must contain package names only separated by white space (including newlines). rpm \-qa \-\-qf='%{name}
+' produces proper output.
+.TP
+.B \-o OUTPUT, \-\-output=OUTPUT
+File to write the result to. Stdout is used if option is omited.
+.TP
+.B \-q, \-\-quiet
+Do not show warnings.
+.TP
+.B \-e, \-\-no\-excludes
+Only show groups that are installed completely. Do not use exclude lines.
+.TP
+.B \-\-global\-excludes
+Print exclude lines at the end and not after the groups requiring them.
+.TP
+.B \-\-global\-addons
+Print package names at the end and not after the groups offering them as addon.
+.TP
+.B \-\-addons\-by\-group
+Also show groups not selected to sort packages contained by them. Those groups are commented out with a "# " at the begin of the line.
+.TP
+.B \-m, \-\-allow\-mandatories
+Check if just installing the mandatory packages gives better results. Uses "." to mark those groups.
+.TP
+.B \-a, \-\-allow\-all
+Check if installing all packages in the groups gives better results. Uses "*" to mark those groups.
+.TP
+.B \-\-ignore\-missing
+Ignore packages missing in the repos.
+.TP
+.B \-\-ignore\-missing\-excludes
+Do not produce exclude lines for packages not in the repository.
+
+.fi
diff --git a/docs/yum-utils.1 b/docs/yum-utils.1
index fe436a7..422b9fb 100644
--- a/docs/yum-utils.1
+++ b/docs/yum-utils.1
@@ -15,6 +15,7 @@ yum\-utils \- tools for manipulating repositories and extended package managemen
 \fBrepotrack\fR \- track packages and its dependencies and downloads them
 \fByum\-builddep\fR \- installs missing dependencies to build a specified package
 \fByum\-complete\-transaction\fR \- finds incomplete or aborted yum transactions and attempts to complete them
+\fByum\-installed\fR \- print a compact package list making use of comps groups 
 \fByumdownloader\fR \- downloads packages from yum repositories including source RPMs
 .SH "DESCRIPTION"
 .B yum\-utils
diff --git a/repoclosure.py b/repoclosure.py
index 53ca63d..1321f84 100755
--- a/repoclosure.py
+++ b/repoclosure.py
@@ -91,10 +91,10 @@ class RepoClosure(yum.YumBase):
             self.repos._selectSackType()
     
     def evrTupletoVer(self,tup):
-        """convert and evr tuple to a version string, return None if nothing
+        """convert an evr tuple to a version string, return None if nothing
         to convert"""
     
-        e, v,r = tup
+        e, v, r = tup
 
         if v is None:
             return None
diff --git a/show-installed.py b/show-installed.py
new file mode 100755
index 0000000..31a6ff7
--- /dev/null
+++ b/show-installed.py
@@ -0,0 +1,409 @@
+#!/usr/bin/python
+
+"""
+TODO:
+ * repository descriptions in kickstart format for non default repos
+"""
+
+import yum
+from optparse import OptionParser
+import sys
+
+__stateprefixes = {
+    None : '# ',
+    "mandatory" : ".",
+    "default" : "@",
+    "all" : "*"
+    }
+
+def state2str(o):
+    if isinstance(o, Group):
+        o = o.state
+    return __stateprefixes[o]
+
+class Group:
+    """Additional information about a comps group"""
+    def __init__(self, compsgroup, yum, state=None):
+        self.id = compsgroup.groupid
+        self.state = None
+        self.compsgroup = compsgroup
+        self.yum = yum
+
+        self.packages = {}
+        for n in (None, "mandatory", "default", "all"):
+            self.packages[n] = {}
+            for s in ("add", "exclude", "exclude_missing", "optional"):
+                self.packages[n][s] = set()
+
+    @property
+    def add(self):
+        """Packages that this group adds to the install"""
+        return self.packages[self.state]["add"]
+
+    @property
+    def exclude(self):
+        """Packages excludes from this group to match the pkg list"""
+        return self.packages[self.state]["exclude"]
+
+    @property
+    def excludeMissing(self):
+        """Excludes that don't have a matching pgk in the repository"""
+        return self.packages[self.state]["exclude_missing"]
+
+    @property
+    def optional(self):
+        """Packages in this group that are not added in the current state"""
+        return self.packages[self.state]["optional"]
+
+    @property
+    def addons(self):
+        """Optional packages that are not added by the group in the given state but nevertheless need to be installed"""
+        return self.packages[self.state]["addons"]
+
+    def _buildSets(self, leafpkgs, allpkgs):
+        # handle conditionals
+        self.conditionals = set()
+        for name, dep in self.compsgroup.conditional_packages.iteritems():
+            if dep in allpkgs:
+                self.conditionals.add(name)
+
+        pkgs = self.conditionals.copy()
+        for name, additionalpkgs in (
+            ("mandatory", self.compsgroup.mandatory_packages),
+            ("default", self.compsgroup.default_packages),
+            ("all", self.compsgroup.optional_packages)):
+            pkgs.update(additionalpkgs)
+            self.__checkGroup(name, pkgs, leafpkgs, allpkgs)
+
+        self.__checkGroup(None, set(), leafpkgs, allpkgs)
+        for name, d in self.packages.iteritems():
+            d["others"] = pkgs - d["add"] - d["exclude"]
+            d["addons"] = d["others"] & leafpkgs
+
+    def __checkGroup(self, name, pkgs, leafpkgs, allpkgs):
+        self.packages[name]["add"] = leafpkgs & pkgs
+        self.packages[name]["exclude"] = pkgs - allpkgs
+        self.packages[name]["exclude_missing"] = set(
+            pkg for pkg in self.packages[name]["exclude"] if not self.yum.pkgSack.searchNames([pkg]))
+        return
+
+    def _autodetectState(self, allowexcludes, allowed=("default",), sharedpkgs=None):
+        """Set state of the group according to the installed packages"""
+        win = None
+        state = self.state
+        for name, d in self.packages.iteritems():
+            if name not in allowed and name is not None:
+                continue
+            if not allowexcludes and d["exclude"]:
+                continue
+            newshared = set()
+            if sharedpkgs:
+                for pkg in d["add"]:
+                    if pkg in sharedpkgs and len(sharedpkgs[pkg]) > 1:
+                        newshared.add(pkg)
+            newwin = len(d["add"]) - len(d["exclude"]) - len(newshared) - 1
+            if win is None or newwin > win:
+                state = name
+                win = newwin
+
+        if win <= 0:
+            state = None
+        # reflect changes in sharedpkgs
+        if state != self.state and sharedpkgs is not None:
+            for pkg in self.packages[self.state]["add"] - self.packages[state]["add"]:
+                if pkg in sharedpkgs:
+                    sharedpkgs[pkg].discard(self)
+            for pkg in self.packages[state]["add"] - self.packages[self.state]["add"]:
+                sharedpkgs.setdefault(pkg, set()).add(self)
+        self.state = state
+
+class InstalledPackages:
+    """Collection of packages and theit interpretation as comps groups."""
+    def __init__(self, yumobj=None, input=None, pkgs=None, ignore_missing=False):
+        """
+        @param yumobj(optional): use this instance of YumBase
+        @param input(optional): read package names from this file
+        @param pkgs(optional): use this iterable of package names
+        @param ignore_missing: exlcude packages not found in the repos
+        """
+
+        if yumobj is None:
+            yumobj = yum.YumBase()
+            yumobj.preconf.debuglevel = 0
+            yumobj.setCacheDir()
+        self.yum = yumobj
+        self.groups = []
+        self.pkg2group = {}
+        self.input = input
+        self.__buildList(pkgs, ignore_missing)
+
+    def __addGroup(self, group):
+        g = Group(group, self.yum)
+        g._buildSets(self.leaves.copy(), self.allpkgs.copy())
+        self.groups.append(g)
+
+    def __evrTupletoVer(self,tup):
+        """convert an evr tuple to a version string, return None if nothing
+        to convert"""
+        e, v, r = tup
+        if v is None:
+            return None
+        val = v
+        if e is not None:
+            val = '%s:%s' % (e, v)
+        if r is not None:
+            val = '%s-%s' % (val, r)
+        return val
+
+    def __getLeaves(self, pkgnames):
+        pkgs = set()
+        missing = set()
+        for name in pkgnames:
+            try:
+                found =  self.yum.pkgSack.returnNewestByName(name)
+                pkgs.update(found) # XXX select proper arch!
+            except yum.Errors.PackageSackError, e:
+                missing.add(name)
+        nonleaves = set()
+        for pkg in pkgs:
+            for (req, flags, (reqe, reqv, reqr)) in pkg.returnPrco('requires'):
+                if req.startswith('rpmlib('): continue # ignore rpmlib deps
+
+                ver = self.__evrTupletoVer((reqe, reqv, reqr))
+                try:
+                    resolve_sack = self.yum.whatProvides(req, flags, ver)
+                except yum.Errors.RepoError, e:
+                    continue
+                for p in resolve_sack:
+                    if p is pkg or p.name == pkg.name:
+                        continue
+                    nonleaves.add(p.name)
+        return pkgnames - nonleaves, missing
+
+    def __buildList(self, pkgs=None, ignore_missing=False):
+        if pkgs:
+            self.allpkgs = frozenset(pkgs)
+        elif self.input is None:
+            self.allpkgs = frozenset(pkg.name for pkg in self.yum.rpmdb.returnPackages())
+        else:
+            pkgs = []
+            for line in self.input:
+                pkgs.extend(line.split())
+            pkgs = map(str.strip, pkgs)
+            pkgs = filter(None, pkgs)
+            self.allpkgs = frozenset(pkgs)
+
+        if self.input is None and not ignore_missing and not pkgs:
+            self.leaves = frozenset((pkg.name for pkg in self.yum.rpmdb.returnLeafNodes()))
+        else:
+            leaves, missing = self.__getLeaves(self.allpkgs)
+            if ignore_missing:
+                self.leaves = leaves - missing
+                self.allpkgs = self.allpkgs - missing
+            else:
+                self.leaves = leaves
+
+        self.leafcount = len(self.leaves)
+
+        # check if package exist in repository
+        self.missingpkgs = set()
+        for pkg in self.allpkgs:
+            if self.yum.pkgSack.searchNames([pkg]):
+                continue
+            self.missingpkgs.add(pkg)
+
+        for group in self.yum.comps.get_groups():
+            self.__addGroup(group)
+
+    def autodetectStates(self, allowexcludes=False, allowed=("default",)):
+        """Check with states (None, "mandatory", "default", "all") is the best
+        for each of the groups.
+        @param allowexcludes: use excludes for groups not installable as
+                              a whole
+        @param allowed: list of states that are considered
+        """
+        pkg2group = {}
+        for g in self.groups:
+            g._autodetectState(allowexcludes, allowed)
+            # find out which pkgs are in more than one group
+            for pkg in g.add:
+                pkg2group.setdefault(pkg, set()).add(g)
+        for g in self.groups:
+            # filter out groups which are not worth it because some of
+            # their packages belong to other groups
+            # This is likely a NP complete problem, but this is a very
+            # simple algorithm. Results may be below the optimum.
+            g._autodetectState(allowexcludes, allowed, self.pkg2group)
+
+    def globalExcludes(self):
+        """return a list of all excludes"""
+        excludes = set()
+        for g in self.groups:
+            excludes.update(g.exclude)
+        return excludes
+
+    def remainingPkgs(self, all=False):
+        """Return a list of all packages not parts of groups
+        or required by others
+        @param all: return the addons of the groups, too
+        """
+        remaining = set(self.leaves)
+        for g in self.groups:
+            remaining.difference_update(g.add)
+            if not all:
+                remaining.difference_update(g.addons)
+        return remaining
+
+class ListPrinter:
+    """Writes things out. Closely coupled to the optparse object
+    created in the main function.
+    """
+    def __init__(self, pkgs, options, output=None):
+        self.pkgs = pkgs
+        if output is None:
+            output = sys.stdout
+        self.output = output
+        self.options = options
+        self.__seen = set()
+
+    def __printPkgs(self, pkgs, prefix='', separator='\n'):
+        pkgs = pkgs - self.__seen
+        self.__seen.update(pkgs)
+        pkgs = list(pkgs)
+        pkgs.sort()
+        for name in pkgs:
+            self.output.write("%s%s%s" % (prefix, name, separator))
+        return len(pkgs)
+
+    def writeWarnings(self):
+        e = sys.stderr
+        if self.pkgs.missingpkgs:
+            e.write("WARNING: The following packages are installed but not in the repository:\n")
+            for pkg in self.pkgs.missingpkgs:
+                e.write("\t%s\n" % pkg)
+            e.write("\n")
+
+        if True:
+            first = True
+            for g in self.pkgs.groups:
+                if not g.excludeMissing:
+                    continue
+                if first:
+                    e.write("WARNING: The following groups contain packages not found in the repositories:\n")
+                    first = False
+                e.write("%s%s\n" % ("XXX ", g.id))
+                for pkg in g.excludeMissing:
+                    e.write("\t%s\n" % pkg)
+            if not first:
+                e.write("\n")
+
+    def writeList(self):
+        self.__seen.clear()
+        if self.options.format == "human":
+            indent = '\t'
+            separator = '\n'
+        elif self.options.format == "kickstart":
+            indent = ''
+            separator = '\n'
+        elif self.options.format == "yum":
+            indent = ''
+            separator = ' '
+        else:
+            raise ValueError("Unknown format")
+
+        remaining = self.pkgs.remainingPkgs(True)
+
+        lines = 0
+        groups = 0
+        for group in self.pkgs.groups:
+            addons = group.addons & remaining
+            if not group.state and not(
+                addons and self.options.addons_by_group and
+                not self.options.global_addons):
+                continue
+            lines += 1
+            if group.state:
+                groups += 1
+
+            self.output.write("%s%s%s" % (state2str(group), group.id, separator))
+            # exclude lines after the group
+            if not self.options.global_excludes:
+                pkgs = group.exclude
+                if self.options.ignore_missing_excludes:
+                    pkgs = pkgs - group.excludeMissing
+                lines += self.__printPkgs(pkgs, indent+'-', separator)
+            # packages after the group
+            if not self.options.global_addons:
+                lines += self.__printPkgs(addons, indent, separator)
+
+        if self.options.format == "human":
+            lines += 1
+            self.output.write("# Others\n")
+
+        # leave filtering out pkgs bmeantioned above to __printPkgs
+        lines += self.__printPkgs(remaining, '', separator)
+
+        # exclude lines at the end
+        excludes = self.pkgs.globalExcludes()
+        if self.options.global_excludes:
+            lines += self.__printPkgs(excludes, '-', separator)
+        # Stats
+        if self.options.format == "human":
+            lines += 3
+            self.output.write("# %i package names, %i leaves\n# %i groups, %i leftovers, %i excludes\n# %i lines\n" % (len(self.pkgs.allpkgs), len(self.pkgs.leaves), groups, len(remaining), len(excludes), lines))
+
+# ****************************************************************************
+
+def __main__():
+    parser = OptionParser(description="Gives a compact description of the packages installed (or given) making use of the comps groups found in the repositories.")
+    parser.add_option("-f", "--format", dest="format",
+                     choices=('kickstart','human','yum'), default="human",
+                     help='yum, kickstart or human; yum gives the result as a yum command line; kickstart the content of a %packages section; "human" readable is default.')
+    parser.add_option("-i", "--input", dest="input", action="store", default=None, help="File to read the package list from instead of using the rpmdb. - for stdin. The file must contain package names only separated by white space (including newlines). rpm -qa --qf='%{name}\n' produces proper output.")
+    parser.add_option("-o", "--output", dest="output", action="store", default=None, help="File to write the result to. Stdout is used if option is omited.")
+    parser.add_option("-q", "--quiet", dest="quiet", action="store_true", help="Do not show warnings.")
+    parser.add_option("-e", "--no-excludes", dest="excludes",
+                      action="store_false", default=True,
+                      help="Only show groups that are installed completely. Do not use exclude lines.")
+
+    parser.add_option("--global-excludes", dest="global_excludes", action="store_true", help="Print exclude lines at the end and not after the groups requiring them.")
+    parser.add_option("--global-addons", dest="global_addons", action="store_true", help="Print package names at the end and not after the groups offering them as addon.")
+    parser.add_option('--addons-by-group', dest="addons_by_group", action="store_true", help='Also show groups not selected to sort packages contained by them. Those groups are commented out with a "# " at the begin of the line.')
+
+    parser.add_option("-m", "--allow-mandatories", dest="allowed", action="append_const", const='mandatory', default=['default'], help='Check if just installing the mandatory packages gives better results. Uses "." to mark those groups.')
+    parser.add_option("-a", "--allow-all", dest="allowed", action='append_const', const='all', help='Check if installing all packages in the groups gives better results. Uses "*" to mark those groups.')
+    parser.add_option("--ignore-missing", dest="ignore_missing", action="store_true", help="Ignore packages missing in the repos.")
+    parser.add_option("--ignore-missing-excludes", dest="ignore_missing_excludes", action="store_true", help="Do not produce exclude lines for packages not in the repository.")
+
+    (options, args) = parser.parse_args()
+
+    if options.format != "human" and len(options.allowed)>1:
+        print '-m, --allow-mandatories, -a, --allow-all are only allowed in "human" (readable) format as yum and anaconda do not support installing all or only mandatory packages per group. Sorry.'
+        sys.exit(-1)
+
+    input_ = None
+    if options.input and options.input=="-":
+        input_ = sys.stdin
+    elif options.input:
+        try:
+            input_ = open(options.input)
+        except IOError, e:
+            print e
+            exit -1
+    else:
+        input_ = None
+    if options.output and options.output!='-':
+        output = open(options.output, "w")
+    else:
+        output = sys.stdout
+
+    i = InstalledPackages(input=input_, ignore_missing=options.ignore_missing)
+    i.autodetectStates(options.excludes, options.allowed)
+
+    p = ListPrinter(i, options, output=output)
+    if not options.quiet:
+        p.writeWarnings()
+    p.writeList()
+
+if __name__ == "__main__":
+    __main__()
diff --git a/yum-utils.spec b/yum-utils.spec
index 3284002..ddb346d 100644
--- a/yum-utils.spec
+++ b/yum-utils.spec
@@ -447,6 +447,7 @@ fi
 %{_bindir}/yum-debug-dump
 %{_bindir}/yum-groups-manager
 %{_bindir}/yum-debug-restore
+%{_bindir}/show-installed
 %{_sbindir}/yum-complete-transaction
 %{_sbindir}/yumdb
 %{_mandir}/man1/yum-utils.1.*


More information about the Yum-commits mailing list