290 lines
9.5 KiB
Python
Executable File
290 lines
9.5 KiB
Python
Executable File
#!/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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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)
|
|
|
|
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_PREQREQ
|
|
# 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")
|
|
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>] <old package> <new package>
|
|
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
|