better-wrong-script.diff
[platform/upstream/rpmlint.git] / rpmdiff
1 #!/usr/bin/python -tt
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright (C) 2006 Mandriva; 2009 Red Hat, Inc.; 2009 Ville Skyttä
5 # Authors: Frederic Lepied, Florian Festi
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU Library General Public License as published by
9 # the Free Software Foundation; version 2 only
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU Library General Public License for more details.
15 #
16 # You should have received a copy of the GNU Library General Public License
17 # along with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 import getopt
21 import itertools
22 import os
23 import site
24 import stat
25 import sys
26 import tempfile
27
28 import rpm
29
30 if os.path.isdir("/usr/share/rpmlint"):
31     site.addsitedir("/usr/share/rpmlint")
32 import Pkg
33
34
35 class Rpmdiff:
36
37     # constants
38
39     TAGS = ( rpm.RPMTAG_NAME, rpm.RPMTAG_SUMMARY,
40              rpm.RPMTAG_DESCRIPTION, rpm.RPMTAG_GROUP,
41              rpm.RPMTAG_LICENSE, rpm.RPMTAG_URL,
42              rpm.RPMTAG_PREIN, rpm.RPMTAG_POSTIN,
43              rpm.RPMTAG_PREUN, rpm.RPMTAG_POSTUN,
44              rpm.RPMTAG_PRETRANS, rpm.RPMTAG_POSTTRANS)
45
46     PRCO = ( 'REQUIRES', 'PROVIDES', 'CONFLICTS', 'OBSOLETES')
47
48     #{fname : (size, mode, mtime, flags, dev, inode,
49     #          nlink, state, vflags, user, group, digest)}
50     __FILEIDX = [ ['S', 0],
51                   ['M', 1],
52                   ['5', 11],
53                   ['D', 4],
54                   ['N', 6],
55                   ['L', 7],
56                   ['V', 8],
57                   ['U', 9],
58                   ['G', 10],
59                   ['F', 3],
60                   ['T', 2] ]
61
62     DEPFORMAT = '%-12s%s %s %s %s'
63     FORMAT = '%-12s%s'
64
65     ADDED   = 'added'
66     REMOVED = 'removed'
67
68     # code starts here
69
70     def __init__(self, old, new, ignore=None):
71         self.result = []
72         self.ignore = ignore
73         if self.ignore is None:
74             self.ignore = []
75
76         FILEIDX = self.__FILEIDX
77         for tag in self.ignore:
78             for entry in FILEIDX:
79                 if tag == entry[0]:
80                     entry[1] = None
81                     break
82
83         try:
84             old = self.__load_pkg(old).header
85             new = self.__load_pkg(new).header
86         except KeyError, e:
87             Pkg.warn(str(e))
88             sys.exit(2)
89
90         # Compare single tags
91         for tag in self.TAGS:
92             old_tag = old[tag]
93             new_tag = new[tag]
94             if old_tag != new_tag:
95                 tagname = rpm.tagnames[tag]
96                 if old_tag == None:
97                     self.__add(self.FORMAT, (self.ADDED, tagname))
98                 elif new_tag == None:
99                     self.__add(self.FORMAT, (self.REMOVED, tagname))
100                 else:
101                     self.__add(self.FORMAT, ('S.5.....', tagname))
102
103         # compare Provides, Requires, ...
104         for  tag in self.PRCO:
105             self.__comparePRCOs(old, new, tag)
106
107         # compare the files
108
109         old_files_dict = self.__fileIteratorToDict(old.fiFromHeader())
110         new_files_dict = self.__fileIteratorToDict(new.fiFromHeader())
111         files = list(set(itertools.chain(old_files_dict.iterkeys(),
112                                          new_files_dict.iterkeys())))
113         files.sort()
114
115         for f in files:
116             diff = False
117
118             old_file = old_files_dict.get(f)
119             new_file = new_files_dict.get(f)
120
121             if not old_file:
122                 self.__add(self.FORMAT, (self.ADDED, f))
123             elif not new_file:
124                 self.__add(self.FORMAT, (self.REMOVED, f))
125             else:
126                 format = ''
127                 for entry in FILEIDX:
128                     if entry[1] != None and \
129                             old_file[entry[1]] != new_file[entry[1]]:
130                         format = format + entry[0]
131                         diff = True
132                     else:
133                         format = format + '.'
134                 if diff:
135                     self.__add(self.FORMAT, (format, f))
136
137     # return a report of the differences
138     def textdiff(self):
139         return '\n'.join((format % data for format, data in self.result))
140
141     # do the two rpms differ
142     def differs(self):
143         return bool(self.result)
144
145     # add one differing item
146     def __add(self, format, data):
147         self.result.append((format, data))
148
149     # load a package from a file or from the installed ones
150     def __load_pkg(self, name, tmpdir = tempfile.gettempdir()):
151         try:
152             st = os.stat(name)
153             if stat.S_ISREG(st[stat.ST_MODE]):
154                 return Pkg.Pkg(name, tmpdir)
155         except (OSError, TypeError):
156             pass
157         inst = Pkg.getInstalledPkgs(name)
158         if not inst:
159             raise KeyError("No installed packages by name %s" % name)
160         if len(inst) > 1:
161             raise KeyError("More than one installed packages by name %s" % name)
162         return inst[0]
163
164     # output the right string according to RPMSENSE_* const
165     def sense2str(self, sense):
166         s = ""
167         for tag, char in ((rpm.RPMSENSE_LESS, "<"),
168                           (rpm.RPMSENSE_GREATER, ">"),
169                           (rpm.RPMSENSE_EQUAL, "=")):
170             if sense & tag:
171                 s += char
172         return s
173
174     # output the right requires string according to RPMSENSE_* const
175     def req2str(self, req):
176         s = "REQUIRES"
177         # we want to use 64 even with rpm versions that define RPMSENSE_PREREQ
178         # as 0 to get sane results when comparing packages built with an old
179         # (64) version and a new (0) one
180         if req & (rpm.RPMSENSE_PREREQ or 64):
181             s = "PREREQ"
182
183         ss = []
184         if req & rpm.RPMSENSE_SCRIPT_PRE:
185             ss.append("pre")
186         elif req & rpm.RPMSENSE_SCRIPT_POST:
187             ss.append("post")
188         elif req & rpm.RPMSENSE_SCRIPT_PREUN:
189             ss.append("preun")
190         elif req & rpm.RPMSENSE_SCRIPT_POSTUN:
191             ss.append("postun")
192         elif req & getattr(rpm, "RPMSENSE_PRETRANS", 1 << 7): # rpm >= 4.9.0
193             ss.append("pretrans")
194         elif req & getattr(rpm, "RPMSENSE_POSTTRANS", 1 << 5): # rpm >= 4.9.0
195             ss.append("posttrans")
196         if ss:
197             s += "(%s)" % ",".join(ss)
198
199         return s
200
201     # compare Provides, Requires, Conflicts, Obsoletes
202     def __comparePRCOs(self, old, new, name):
203         oldflags = old[name[:-1]+'FLAGS']
204         newflags = new[name[:-1]+'FLAGS']
205         # fix buggy rpm binding not returning list for single entries
206         if not isinstance(oldflags, list): oldflags = [ oldflags ]
207         if not isinstance(newflags, list): newflags = [ newflags ]
208
209         o = zip(old[name], oldflags, old[name[:-1]+'VERSION'])
210         n = zip(new[name], newflags, new[name[:-1]+'VERSION'])
211
212         # filter self provides, TODO: self %name(%_isa) as well
213         if name == 'PROVIDES':
214             oldE = old['epoch'] is not None and str(old['epoch'])+":" or ""
215             oldNV = (old['name'], rpm.RPMSENSE_EQUAL,
216                      "%s%s-%s" % (oldE, old['version'], old['release']))
217             newE = new['epoch'] is not None and str(new['epoch'])+":" or ""
218             newNV = (new['name'], rpm.RPMSENSE_EQUAL,
219                      "%s%s-%s" % (newE, new['version'], new['release']))
220             o = [entry for entry in o if entry != oldNV]
221             n = [entry for entry in n if entry != newNV]
222
223         for oldentry in o:
224             if not oldentry in n:
225                 namestr = name
226                 if namestr == 'REQUIRES':
227                     namestr = self.req2str(oldentry[1])
228                 self.__add(self.DEPFORMAT,
229                            (self.REMOVED, namestr, oldentry[0],
230                             self.sense2str(oldentry[1]), oldentry[2]))
231         for newentry in n:
232             if not newentry in o:
233                 namestr = name
234                 if namestr == 'REQUIRES':
235                     namestr = self.req2str(newentry[1])
236                 self.__add(self.DEPFORMAT,
237                            (self.ADDED, namestr, newentry[0],
238                             self.sense2str(newentry[1]), newentry[2]))
239
240     def __fileIteratorToDict(self, fi):
241         result = {}
242         for filedata in fi:
243             result[filedata[0]] = filedata[1:]
244         return result
245
246 def _usage(exit=1):
247     print ('''Usage: %s [<options>] <old package> <new package>
248 Options:
249   -h, --help     Output this message and exit
250   -i, --ignore   File property to ignore when calculating differences (may be
251                  used multiple times); valid values are: S (size), M (mode),
252                  5 (checksum), D (device), N (inode), L (number of links),
253                  V (vflags), U (user), G (group), F (digest), T (time)''' \
254         % sys.argv[0])
255     sys.exit(exit)
256
257 def main():
258
259     ignore_tags = []
260     try:
261         opts, args = getopt.getopt(sys.argv[1:],
262                                    "hti:", ["help", "ignore-times", "ignore="])
263     except getopt.GetoptError, e:
264         Pkg.warn("Error: %s" % e)
265         _usage()
266
267     for option, argument in opts:
268         if option in ("-h", "--help"):
269             _usage(0)
270         if option in ("-t", "--ignore-times"):
271             # deprecated; --ignore=T should be used instead
272             ignore_tags.append("T")
273         if option in ("-i", "--ignore"):
274             ignore_tags.append(argument)
275
276     if len(args) != 2:
277         _usage()
278
279     d = Rpmdiff(args[0], args[1], ignore=ignore_tags)
280     textdiff = d.textdiff()
281     if textdiff:
282         print (textdiff)
283     sys.exit(int(d.differs()))
284
285 if __name__ == '__main__':
286     main()
287
288 # rpmdiff ends here
289
290 # Local variables:
291 # indent-tabs-mode: nil
292 # py-indent-offset: 4
293 # End:
294 # ex: ts=4 sw=4 et