#!/usr/bin/python -tt # -*- coding: utf-8 -*- # # Copyright (C) 2006 Mandriva; 2009 Red Hat, Inc.; 2009 Ville Skyttä # Authors: Frederic Lepied, Florian Festi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Library General Public License as published by # the Free Software Foundation; version 2 only # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU Library General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import getopt import itertools import os import site import stat import sys import tempfile import rpm if os.path.isdir("/usr/share/rpmlint"): site.addsitedir("/usr/share/rpmlint") import Pkg class Rpmdiff: # constants TAGS = ( rpm.RPMTAG_NAME, rpm.RPMTAG_SUMMARY, rpm.RPMTAG_DESCRIPTION, rpm.RPMTAG_GROUP, rpm.RPMTAG_LICENSE, rpm.RPMTAG_URL, rpm.RPMTAG_PREIN, rpm.RPMTAG_POSTIN, rpm.RPMTAG_PREUN, rpm.RPMTAG_POSTUN, rpm.RPMTAG_PRETRANS, rpm.RPMTAG_POSTTRANS) PRCO = ( 'REQUIRES', 'PROVIDES', 'CONFLICTS', 'OBSOLETES') #{fname : (size, mode, mtime, flags, dev, inode, # nlink, state, vflags, user, group, digest)} __FILEIDX = [ ['S', 0], ['M', 1], ['5', 11], ['D', 4], ['N', 6], ['L', 7], ['V', 8], ['U', 9], ['G', 10], ['F', 3], ['T', 2] ] DEPFORMAT = '%-12s%s %s %s %s' FORMAT = '%-12s%s' ADDED = 'added' REMOVED = 'removed' # code starts here def __init__(self, old, new, ignore=None): self.result = [] self.ignore = ignore if self.ignore is None: self.ignore = [] FILEIDX = self.__FILEIDX for tag in self.ignore: for entry in FILEIDX: if tag == entry[0]: entry[1] = None break try: old = self.__load_pkg(old).header new = self.__load_pkg(new).header except KeyError, e: Pkg.warn(str(e)) sys.exit(2) # Compare single tags for tag in self.TAGS: old_tag = old[tag] new_tag = new[tag] if old_tag != new_tag: tagname = rpm.tagnames[tag] if old_tag == None: self.__add(self.FORMAT, (self.ADDED, tagname)) elif new_tag == None: self.__add(self.FORMAT, (self.REMOVED, tagname)) else: self.__add(self.FORMAT, ('S.5.....', tagname)) # compare Provides, Requires, ... for tag in self.PRCO: self.__comparePRCOs(old, new, tag) # compare the files old_files_dict = self.__fileIteratorToDict(old.fiFromHeader()) new_files_dict = self.__fileIteratorToDict(new.fiFromHeader()) files = list(set(itertools.chain(old_files_dict.iterkeys(), new_files_dict.iterkeys()))) files.sort() for f in files: diff = False old_file = old_files_dict.get(f) new_file = new_files_dict.get(f) if not old_file: self.__add(self.FORMAT, (self.ADDED, f)) elif not new_file: self.__add(self.FORMAT, (self.REMOVED, f)) else: format = '' for entry in FILEIDX: if entry[1] != None and \ old_file[entry[1]] != new_file[entry[1]]: format = format + entry[0] diff = True else: format = format + '.' if diff: self.__add(self.FORMAT, (format, f)) # return a report of the differences def textdiff(self): return '\n'.join((format % data for format, data in self.result)) # do the two rpms differ def differs(self): return bool(self.result) # add one differing item def __add(self, format, data): self.result.append((format, data)) # load a package from a file or from the installed ones def __load_pkg(self, name, tmpdir = tempfile.gettempdir()): try: st = os.stat(name) if stat.S_ISREG(st[stat.ST_MODE]): return Pkg.Pkg(name, tmpdir) except (OSError, TypeError): pass inst = Pkg.getInstalledPkgs(name) if not inst: raise KeyError("No installed packages by name %s" % name) if len(inst) > 1: raise KeyError("More than one installed packages by name %s" % name) return inst[0] # output the right string according to RPMSENSE_* const def sense2str(self, sense): s = "" for tag, char in ((rpm.RPMSENSE_LESS, "<"), (rpm.RPMSENSE_GREATER, ">"), (rpm.RPMSENSE_EQUAL, "=")): if sense & tag: s += char return s # output the right requires string according to RPMSENSE_* const def req2str(self, req): s = "REQUIRES" # we want to use 64 even with rpm versions that define RPMSENSE_PREREQ # as 0 to get sane results when comparing packages built with an old # (64) version and a new (0) one if req & (rpm.RPMSENSE_PREREQ or 64): s = "PREREQ" ss = [] if req & rpm.RPMSENSE_SCRIPT_PRE: ss.append("pre") elif req & rpm.RPMSENSE_SCRIPT_POST: ss.append("post") elif req & rpm.RPMSENSE_SCRIPT_PREUN: ss.append("preun") elif req & rpm.RPMSENSE_SCRIPT_POSTUN: ss.append("postun") elif req & getattr(rpm, "RPMSENSE_PRETRANS", 1 << 7): # rpm >= 4.9.0 ss.append("pretrans") elif req & getattr(rpm, "RPMSENSE_POSTTRANS", 1 << 5): # rpm >= 4.9.0 ss.append("posttrans") if ss: s += "(%s)" % ",".join(ss) return s # compare Provides, Requires, Conflicts, Obsoletes def __comparePRCOs(self, old, new, name): oldflags = old[name[:-1]+'FLAGS'] newflags = new[name[:-1]+'FLAGS'] # fix buggy rpm binding not returning list for single entries if not isinstance(oldflags, list): oldflags = [ oldflags ] if not isinstance(newflags, list): newflags = [ newflags ] o = zip(old[name], oldflags, old[name[:-1]+'VERSION']) n = zip(new[name], newflags, new[name[:-1]+'VERSION']) # filter self provides, TODO: self %name(%_isa) as well if name == 'PROVIDES': oldE = old['epoch'] is not None and str(old['epoch'])+":" or "" oldNV = (old['name'], rpm.RPMSENSE_EQUAL, "%s%s-%s" % (oldE, old['version'], old['release'])) newE = new['epoch'] is not None and str(new['epoch'])+":" or "" newNV = (new['name'], rpm.RPMSENSE_EQUAL, "%s%s-%s" % (newE, new['version'], new['release'])) o = [entry for entry in o if entry != oldNV] n = [entry for entry in n if entry != newNV] for oldentry in o: if not oldentry in n: namestr = name if namestr == 'REQUIRES': namestr = self.req2str(oldentry[1]) self.__add(self.DEPFORMAT, (self.REMOVED, namestr, oldentry[0], self.sense2str(oldentry[1]), oldentry[2])) for newentry in n: if not newentry in o: namestr = name if namestr == 'REQUIRES': namestr = self.req2str(newentry[1]) self.__add(self.DEPFORMAT, (self.ADDED, namestr, newentry[0], self.sense2str(newentry[1]), newentry[2])) def __fileIteratorToDict(self, fi): result = {} for filedata in fi: result[filedata[0]] = filedata[1:] return result def _usage(exit=1): print ('''Usage: %s [] Options: -h, --help Output this message and exit -i, --ignore File property to ignore when calculating differences (may be used multiple times); valid values are: S (size), M (mode), 5 (checksum), D (device), N (inode), L (number of links), V (vflags), U (user), G (group), F (digest), T (time)''' \ % sys.argv[0]) sys.exit(exit) def main(): ignore_tags = [] try: opts, args = getopt.getopt(sys.argv[1:], "hti:", ["help", "ignore-times", "ignore="]) except getopt.GetoptError, e: Pkg.warn("Error: %s" % e) _usage() for option, argument in opts: if option in ("-h", "--help"): _usage(0) if option in ("-t", "--ignore-times"): # deprecated; --ignore=T should be used instead ignore_tags.append("T") if option in ("-i", "--ignore"): ignore_tags.append(argument) if len(args) != 2: _usage() d = Rpmdiff(args[0], args[1], ignore=ignore_tags) textdiff = d.textdiff() if textdiff: print (textdiff) sys.exit(int(d.differs())) if __name__ == '__main__': main() # rpmdiff ends here # Local variables: # indent-tabs-mode: nil # py-indent-offset: 4 # End: # ex: ts=4 sw=4 et