Add --diff and --diff-prog options
[opensuse:spec-cleaner.git] / spec-cleaner
1 #!/usr/bin/env python
2 # vim: set ts=4 sw=4 et: coding=UTF-8
3
4 #
5 # Copyright (c) 2009, Novell, Inc.
6 # All rights reserved.
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are met:
10 #
11 #  * Redistributions of source code must retain the above copyright notice,
12 #    this list of conditions and the following disclaimer.
13 #  * Redistributions in binary form must reproduce the above copyright notice,
14 #    this list of conditions and the following disclaimer in the documentation
15 #    and/or other materials provided with the distribution.
16 #  * Neither the name of the <ORGANIZATION> nor the names of its contributors
17 #    may be used to endorse or promote products derived from this software
18 #    without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 # POSSIBILITY OF SUCH DAMAGE.
31 #
32 #
33 # (Licensed under the simplified BSD license)
34 #
35 # Authors:
36 #   Vincent Untz <vuntz@novell.com>
37 #   Pavol Rusnak <prusnak@opensuse.org>
38 #   Petr Uzel <petr.uzel@suse.cz>
39 #
40
41 import os
42 import sys
43
44 import cStringIO
45 import optparse
46 import re
47 import time
48 import tempfile
49 import subprocess
50 import shlex
51
52 #######################################################################
53
54 VERSION = '0.1'
55
56 re_comment = re.compile('^$|^\s*#')
57 re_define = re.compile('^\s*%define', re.IGNORECASE)
58
59 re_bindir = re.compile('%{_prefix}/bin([/\s$])')
60 re_sbindir = re.compile('%{_prefix}/sbin([/\s$])')
61 re_includedir = re.compile('%{_prefix}/include([/\s$])')
62 re_datadir = re.compile('%{_prefix}/share([/\s$])')
63 re_mandir = re.compile('%{_datadir}/man([/\s$])')
64 re_infodir = re.compile('%{_datadir}/info([/\s$])')
65
66
67 def strip_useless_spaces(s):
68     return ' '.join(s.split())
69
70
71 def replace_known_dirs(s):
72     s = s.replace('%_prefix', '%{_prefix}')
73     s = s.replace('%_usr', '%{_prefix}')
74     s = s.replace('%{_usr}', '%{_prefix}')
75     s = s.replace('%_bindir', '%{_bindir}')
76     s = s.replace('%_sbindir', '%{_sbindir}')
77     s = s.replace('%_includedir', '%{_includedir}')
78     s = s.replace('%_datadir', '%{_datadir}')
79     s = s.replace('%_mandir', '%{_mandir}')
80     s = s.replace('%_infodir', '%{_infodir}')
81     s = s.replace('%_libdir', '%{_libdir}')
82     s = s.replace('%_libexecdir', '%{_libexecdir}')
83     s = s.replace('%_lib', '%{_lib}')
84     s = s.replace('%{_prefix}/%{_lib}', '%{_libdir}')
85     s = s.replace('%_sysconfdir', '%{_sysconfdir}')
86     s = s.replace('%_localstatedir', '%{_localstatedir}')
87     s = s.replace('%_var', '%{_localstatedir}')
88     s = s.replace('%{_var}', '%{_localstatedir}')
89     s = s.replace('%_initddir', '%{_initddir}')
90     # old typo in rpm macro
91     s = s.replace('%_initrddir', '%{_initddir}')
92     s = s.replace('%{_initrddir}', '%{_initddir}')
93
94     s = re_bindir.sub(r'%{_bindir}\1', s)
95     s = re_sbindir.sub(r'%{_sbindir}\1', s)
96     s = re_includedir.sub(r'%{_includedir}\1', s)
97     s = re_datadir.sub(r'%{_datadir}\1', s)
98     s = re_mandir.sub(r'%{_mandir}\1', s)
99     s = re_infodir.sub(r'%{_infodir}\1', s)
100
101     return s
102
103
104 def replace_buildroot(s):
105     s = s.replace('${RPM_BUILD_ROOT}', '%{buildroot}')
106     s = s.replace('$RPM_BUILD_ROOT', '%{buildroot}')
107     s = s.replace('%buildroot', '%{buildroot}')
108     s = s.replace('%{buildroot}/etc/init.d/', '%{buildroot}%{_initddir}/')
109     s = s.replace('%{buildroot}/etc/', '%{buildroot}%{_sysconfdir}/')
110     s = s.replace('%{buildroot}/usr/', '%{buildroot}%{_prefix}/')
111     s = s.replace('%{buildroot}/var/', '%{buildroot}%{_localstatedir}/')
112     s = s.replace('"%{buildroot}"', '%{buildroot}')
113     return s
114
115
116 def replace_optflags(s):
117     s = s.replace('${RPM_OPT_FLAGS}', '%{optflags}')
118     s = s.replace('$RPM_OPT_FLAGS', '%{optflags}')
119     s = s.replace('%optflags', '%{optflags}')
120     return s
121
122
123 def replace_remove_la(s):
124     cmp_line = strip_useless_spaces(s)
125     if cmp_line in [ 'find %{buildroot} -type f -name "*.la" -exec %{__rm} -fv {} +', 'find %{buildroot} -type f -name "*.la" -delete' ]:
126         s = 'find %{buildroot} -type f -name "*.la" -delete -print'
127     return s
128
129
130 def replace_utils(s):
131     # take care of all utilities macros that bloat spec file
132     r = {'id_u': 'id -u', 'ln_s': 'ln -s', 'lzma': 'xz --format-lzma', 'mkdir_p': 'mkdir -p', 'awk':'gawk', 'cc':'gcc', 'cpp':'gcc -E', 'cxx':'g++', 'remsh':'rsh', }
133     for i in r:
134       s = s.replace('%__' + i, r[i])
135       s = s.replace('%{__' + i + '}', r[i])
136
137     for i in [ 'aclocal', 'ar', 'as', 'autoconf', 'autoheader', 'automake', 'bzip2', 'cat', 'chgrp', 'chmod', 'chown', 'cp', 'cpio', 'file', 'gpg', 'grep', 'gzip', 'id', 'install', 'ld', 'libtoolize', 'make', 'mkdir', 'mv', 'nm', 'objcopy', 'objdump', 'patch', 'perl', 'python', 'ranlib', 'restorecon', 'rm', 'rsh', 'sed', 'semodule', 'ssh', 'strip', 'tar', 'unzip', 'xz', ]:
138         s = s.replace('%__' + i, i)
139         s = s.replace('%{__' + i + '}', i)
140
141     return s
142
143
144 def replace_buildservice(s):
145     for i in ['centos', 'debian', 'fedora', 'mandriva', 'meego', 'rhel', 'sles', 'suse', 'ubuntu']:
146         s = s.replace('%' + i + '_version', '0%{?' + i + '_version}')
147         s = s.replace('%{' + i + '_version}', '0%{?' + i + '_version}')
148     return s
149
150
151 def replace_all(s):
152     s = replace_buildroot(s)
153     s = replace_optflags(s)
154     s = replace_known_dirs(s)
155     s = replace_remove_la(s)
156     s = replace_utils(s)
157     s = replace_buildservice(s)
158     return s
159
160
161 #######################################################################
162
163
164 class RpmException(Exception):
165     pass
166
167
168 #######################################################################
169
170
171 class RpmSection(object):
172     '''
173         Basic cleanup: we remove trailing spaces.
174     '''
175
176     def __init__(self):
177         self.lines = []
178         self.previous_line = None
179
180     def add(self, line):
181         line = line.rstrip()
182         line = replace_all(line)
183         self.lines.append(line)
184         self.previous_line = line
185
186     def output(self, fout):
187         for line in self.lines:
188             fout.write(line + '\n')
189
190
191 #######################################################################
192
193
194 class RpmCopyright(RpmSection):
195     '''
196         Adds default copyright notice if needed.
197         Remove initial empty lines.
198         Remove norootforbuild.
199     '''
200
201
202     def _add_default_copyright(self):
203         self.lines.append(time.strftime('''#
204 # Please submit bugfixes or comments via http://bugs.opensuse.org/
205 #
206 '''))
207
208
209     def add(self, line):
210         if not self.lines and not line:
211             return
212
213         if line.startswith('# norootforbuild') or \
214            line.startswith('# usedforbuild'):
215             return
216
217         RpmSection.add(self, line)
218
219
220     def output(self, fout):
221         if not self.lines:
222             self._add_default_copyright()
223         RpmSection.output(self, fout)
224
225
226 #######################################################################
227
228
229 class RpmPreamble(RpmSection):
230     '''
231         Only keep one empty line for many consecutive ones.
232         Reorder lines.
233         Fix bad licenses.
234         Use one line per BuildRequires/Requires/etc.
235         Use %{version} instead of %{version}-%{release} for BuildRequires/etc.
236         Remove AutoReqProv.
237         Standardize BuildRoot.
238
239         This one is a bit tricky since we reorder things. We have a notion of
240         paragraphs, categories, and groups.
241
242         A paragraph is a list of non-empty lines. Conditional directives like
243         %if/%else/%endif also mark paragraphs. It contains categories.
244         A category is a list of lines on the same topic. It contains a list of
245         groups.
246         A group is a list of lines where the first few ones are either %define
247         or comment lines, and the last one is a normal line.
248
249         This means that the %define and comments will stay attached to one
250         line, even if we reorder the lines.
251     '''
252
253     re_if = re.compile('^\s*(?:%if\s|%ifarch\s|%ifnarch\s|%else\s*$|%endif\s*$)', re.IGNORECASE)
254
255     re_name = re.compile('^Name:\s*(\S*)', re.IGNORECASE)
256     re_version = re.compile('^Version:\s*(\S*)', re.IGNORECASE)
257     re_release = re.compile('^Release:\s*(\S*)', re.IGNORECASE)
258     re_license = re.compile('^License:\s*(.*)', re.IGNORECASE)
259     re_summary = re.compile('^Summary:\s*(.*)', re.IGNORECASE)
260     re_url = re.compile('^Url:\s*(\S*)', re.IGNORECASE)
261     re_group = re.compile('^Group:\s*(.*)', re.IGNORECASE)
262     re_source = re.compile('^Source(\d*):\s*(\S*)', re.IGNORECASE)
263     re_patch = re.compile('^((?:#[#\s]*)?)Patch(\d*):\s*(\S*)', re.IGNORECASE)
264     re_buildrequires = re.compile('^BuildRequires:\s*(.*)', re.IGNORECASE)
265     re_prereq = re.compile('^PreReq:\s*(.*)', re.IGNORECASE)
266     re_requires = re.compile('^Requires:\s*(.*)', re.IGNORECASE)
267     re_recommends = re.compile('^Recommends:\s*(.*)', re.IGNORECASE)
268     re_suggests = re.compile('^Suggests:\s*(.*)', re.IGNORECASE)
269     re_supplements = re.compile('^Supplements:\s*(.*)', re.IGNORECASE)
270     re_provides = re.compile('^Provides:\s*(.*)', re.IGNORECASE)
271     re_obsoletes = re.compile('^Obsoletes:\s*(.*)', re.IGNORECASE)
272     re_buildroot = re.compile('^\s*BuildRoot:', re.IGNORECASE)
273     re_buildarch = re.compile('^\s*BuildArch:\s*(.*)', re.IGNORECASE)
274
275     re_requires_token = re.compile('(\s*(\S+(?:\s*(?:[<>]=?|=)\s*[^\s,]+)?),?)')
276
277     category_to_re = {
278         'name': re_name,
279         'version': re_version,
280         'release': re_release,
281         'license': re_license,
282         'summary': re_summary,
283         'url': re_url,
284         'group': re_group,
285         # for source, we have a special match to keep the source number
286         # for patch, we have a special match to keep the patch number
287         'buildrequires': re_buildrequires,
288         'prereq': re_prereq,
289         'requires': re_requires,
290         'recommends': re_recommends,
291         'suggests': re_suggests,
292         'supplements': re_supplements,
293         # for provides/obsoletes, we have a special case because we group them
294         # for build root, we have a special match because we force its value
295         'buildarch': re_buildarch
296     }
297
298     category_to_key = {
299         'name': 'Name',
300         'version': 'Version',
301         'release': 'Release',
302         'license': 'License',
303         'summary': 'Summary',
304         'url': 'Url',
305         'group': 'Group',
306         'source': 'Source',
307         'patch': 'Patch',
308         'buildrequires': 'BuildRequires',
309         'prereq': 'PreReq',
310         'requires': 'Requires',
311         'recommends': 'Recommends',
312         'suggests': 'Suggests',
313         'supplements': 'Supplements',
314         # Provides/Obsoletes cannot be part of this since we want to keep them
315         # mixed, so we'll have to specify the key when needed
316         'buildroot': 'BuildRoot',
317         'buildarch': 'BuildArch'
318     }
319
320     category_to_fixer = {
321     }
322
323     license_fixes = {
324         'LGPL v2.0 only': 'LGPLv2.0',
325         'LGPL v2.0 or later': 'LGPLv2.0+',
326         'LGPL v2.1 only': 'LGPLv2.1',
327         'LGPL v2.1 or later': 'LGPLv2.1+',
328         'LGPL v3 only': 'LGPLv3',
329         'LGPL v3 or later': 'LGPLv3+',
330         'GPL v2 only': 'GPLv2',
331         'GPL v2 or later': 'GPLv2+',
332         'GPL v3 only': 'GPLv3',
333         'GPL v3 or later': 'GPLv3+'
334     }
335
336     categories_order = [ 'name', 'version', 'release', 'license', 'summary', 'url', 'group', 'source', 'patch', 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements', 'provides_obsoletes', 'buildroot', 'buildarch', 'misc' ]
337
338     categories_with_sorted_package_tokens = [ 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements' ]
339     categories_with_package_tokens = categories_with_sorted_package_tokens[:]
340     categories_with_package_tokens.append('provides_obsoletes')
341
342     re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE)
343
344
345     def __init__(self):
346         RpmSection.__init__(self)
347         self._start_paragraph()
348
349
350     def _start_paragraph(self):
351         self.paragraph = {}
352         for i in self.categories_order:
353             self.paragraph[i] = []
354         self.current_group = []
355
356
357     def _add_group(self, group):
358         t = type(group)
359
360         if t == str:
361             RpmSection.add(self, group)
362         elif t == list:
363             for subgroup in group:
364                 self._add_group(subgroup)
365         else:
366             raise RpmException('Unknown type of group in preamble: %s' % t)
367
368
369     def _end_paragraph(self):
370         def sort_helper_key(a):
371             t = type(a)
372             if t == str:
373                 return a
374             elif t == list:
375                 return a[-1]
376             else:
377                 raise RpmException('Unknown type during sort: %s' % t)
378
379         for i in self.categories_order:
380             if i in self.categories_with_sorted_package_tokens:
381                 self.paragraph[i].sort(key=sort_helper_key)
382             for group in self.paragraph[i]:
383                 self._add_group(group)
384         if self.current_group:
385             # the current group was not added to any category. It's just some
386             # random stuff that should be at the end anyway.
387             self._add_group(self.current_group)
388
389         self._start_paragraph()
390
391
392     def _fix_license(self, value):
393         licenses = value.split(';')
394         for (index, license) in enumerate(licenses):
395             license = strip_useless_spaces(license)
396             if self.license_fixes.has_key(license):
397                 license = self.license_fixes[license]
398             licenses[index] = license
399
400         return [ ' ; '.join(licenses) ]
401
402     category_to_fixer['license'] = _fix_license
403
404
405     def _pkgname_to_pkgconfig(self, value):
406         r = {
407           'dbus-1-devel': 'dbus-1',
408           'dbus-1-glib-devel': 'dbus-glib-1',
409           'exo-devel': 'exo-1',
410           'glib2-devel': 'glib-2.0',
411           'gtk2-devel': 'gtk-2.0',
412           'libgarcon-devel': 'garcon-1',
413           'libglade2-devel': 'libglade-2.0',
414           'libnotify-devel': 'libnotify',
415           'libwnck-devel': 'libwnck-1.0',
416           'libxfce4ui-devel': 'libxfce4ui-1',
417           'libxfce4util-devel': 'libxfce4util-1.0',
418           'libxfcegui4-devel': 'libxfcegui4-1.0',
419           'libxfconf-devel': 'libxfconf-0',
420           'startup-notification-devel': 'libstartup-notification-1.0',
421         }
422         for i in r:
423             value = value.replace(i, 'pkgconfig('+r[i]+')')
424         return value
425
426     def _fix_list_of_packages(self, value):
427         if self.re_requires_token.match(value):
428             tokens = [ item[1] for item in self.re_requires_token.findall(value) ]
429             for (index, token) in enumerate(tokens):
430                 token = token.replace('%{version}-%{release}', '%{version}')
431                 token = token.replace(' ','')
432                 token = re.sub(r'([<>]=?|=)', r' \1 ', token)
433                 token = self._pkgname_to_pkgconfig(token)
434                 tokens[index] = token
435
436             tokens.sort()
437             return tokens
438         else:
439             return [ value ]
440
441     for i in categories_with_package_tokens:
442         category_to_fixer[i] = _fix_list_of_packages
443
444
445     def _add_line_value_to(self, category, value, key = None):
446         """
447             Change a key-value line, to make sure we have the right spacing.
448
449             Note: since we don't have a key <-> category matching, we need to
450             redo one. (Eg: Provides and Obsoletes are in the same category)
451         """
452         keylen = len('BuildRequires:  ')
453
454         if key:
455             pass
456         elif self.category_to_key.has_key(category):
457             key = self.category_to_key[category]
458         else:
459             raise RpmException('Unhandled category in preamble: %s' % category)
460
461         key += ':'
462         while len(key) < keylen:
463             key += ' '
464
465         if self.category_to_fixer.has_key(category):
466             values = self.category_to_fixer[category](self, value)
467         else:
468             values = [ value ]
469
470         for value in values:
471             line = key + value
472             self._add_line_to(category, line)
473
474
475     def _add_line_to(self, category, line):
476         if self.current_group:
477             self.current_group.append(line)
478             self.paragraph[category].append(self.current_group)
479             self.current_group = []
480         else:
481             self.paragraph[category].append(line)
482
483         self.previous_line = line
484
485
486     def add(self, line):
487         if len(line) == 0:
488             if not self.previous_line or len(self.previous_line) == 0:
489                 return
490
491             # we put the empty line in the current group (so we don't list it),
492             # and write the paragraph
493             self.current_group.append(line)
494             self._end_paragraph()
495             self.previous_line = line
496             return
497
498         elif self.re_if.match(line):
499             # %if/%else/%endif marks the end of the previous paragraph
500             # We append the line at the end of the previous paragraph, though,
501             # since it will stay at the end there. If putting it at the
502             # beginning of the next paragraph, it will likely move (with the
503             # misc category).
504             self.current_group.append(line)
505             self._end_paragraph()
506             self.previous_line = line
507             return
508
509         elif re_comment.match(line) or re_define.match(line):
510             self.current_group.append(line)
511             self.previous_line = line
512             return
513
514         elif self.re_autoreqprov.match(line):
515             return
516
517         elif self.re_source.match(line):
518             match = self.re_source.match(line)
519             self._add_line_value_to('source', match.group(2), key = 'Source%s' % match.group(1))
520             return
521
522         elif self.re_patch.match(line):
523             # FIXME: this is not perfect, but it's good enough for most cases
524             if not self.previous_line or not re_comment.match(self.previous_line):
525                 self.current_group.append('# PATCH-MISSING-TAG -- See http://en.opensuse.org/Packaging/Patches')
526
527             match = self.re_patch.match(line)
528             # convert Patch: to Patch0:
529             if match.group(2) == '':
530                 zero = '0'
531             else:
532                 zero = ''
533             self._add_line_value_to('source', match.group(3), key = '%sPatch%s%s' % (match.group(1), zero, match.group(2)))
534             return
535
536         elif self.re_provides.match(line):
537             match = self.re_provides.match(line)
538             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Provides')
539             return
540
541         elif self.re_obsoletes.match(line):
542             match = self.re_obsoletes.match(line)
543             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Obsoletes')
544             return
545
546         elif self.re_buildroot.match(line):
547             if len(self.paragraph['buildroot']) == 0:
548                 self._add_line_value_to('buildroot', '%{_tmppath}/%{name}-%{version}-build')
549             return
550
551         else:
552             for (category, regexp) in self.category_to_re.iteritems():
553                 match = regexp.match(line)
554                 if match:
555                     self._add_line_value_to(category, match.group(1))
556                     return
557
558             self._add_line_to('misc', line)
559
560
561     def output(self, fout):
562         self._end_paragraph()
563         RpmSection.output(self, fout)
564
565
566 #######################################################################
567
568
569 class RpmPackage(RpmPreamble):
570     '''
571         We handle this the same was as the preamble.
572     '''
573
574     def add(self, line):
575         # The first line (%package) should always be added and is different
576         # from the lines we handle in RpmPreamble.
577         if self.previous_line is None:
578             RpmSection.add(self, line)
579             return
580
581         RpmPreamble.add(self, line)
582
583
584 #######################################################################
585
586
587 class RpmDescription(RpmSection):
588     '''
589         Only keep one empty line for many consecutive ones.
590         Remove Authors from description.
591     '''
592
593     def __init__(self):
594         RpmSection.__init__(self)
595         self.removing_authors = False
596         # Tracks the use of a macro. When this happens and we're still in a
597         # description, we actually don't know where we are so we just put all
598         # the following lines blindly, without trying to fix anything.
599         self.unknown_line = False
600
601     def add(self, line):
602         lstrip = line.lstrip()
603         if self.previous_line != None and len(lstrip) > 0 and lstrip[0] == '%':
604             self.unknown_line = True
605
606         if self.removing_authors and not self.unknown_line:
607             return
608
609         if len(line) == 0:
610             if not self.previous_line or len(self.previous_line) == 0:
611                 return
612
613         if line == 'Authors:':
614             self.removing_authors = True
615             return
616
617         RpmSection.add(self, line)
618
619
620 #######################################################################
621
622
623 class RpmPrep(RpmSection):
624     '''
625         Try to simplify to %setup -q when possible.
626         Replace %patch with %patch0
627     '''
628
629     def add(self, line):
630         if line.startswith('%setup'):
631             cmp_line = line.replace(' -q', '')
632             cmp_line = cmp_line.replace(' -n %{name}-%{version}', '')
633             cmp_line = strip_useless_spaces(cmp_line)
634             if cmp_line == '%setup':
635                 line = '%setup -q'
636         if line.startswith('%patch '):
637             line = line.replace('%patch','%patch0')
638
639         RpmSection.add(self, line)
640
641
642 #######################################################################
643
644
645 class RpmBuild(RpmSection):
646     '''
647         Replace %{?jobs:-j%jobs} (suse-ism) with %{?_smp_mflags}
648     '''
649
650     def add(self, line):
651         if not re_comment.match(line):
652             line = line.replace('%_smp_mflags'     , '%{?_smp_mflags}')
653             line = line.replace('%{_smp_mflags}'   , '%{?_smp_mflags}')
654             line = line.replace('%{?jobs:-j%jobs}' , '%{?_smp_mflags}')
655             line = line.replace('%{?jobs: -j%jobs}', '%{?_smp_mflags}')
656             line = line.replace('%{?jobs:-j %jobs}', '%{?_smp_mflags}')
657
658         RpmSection.add(self, line)
659
660
661 #######################################################################
662
663
664 class RpmInstall(RpmSection):
665     '''
666         Remove commands that wipe out the build root.
667         Use %make_install macro.
668         Replace %makeinstall (suse-ism).
669     '''
670
671     def add(self, line):
672         # remove double spaces when comparing the line
673         cmp_line = strip_useless_spaces(line)
674         cmp_line = replace_buildroot(cmp_line)
675
676         if cmp_line.find('DESTDIR=%{buildroot}') != -1:
677             buf = cmp_line.replace('DESTDIR=%{buildroot}', '')
678             buf = strip_useless_spaces(buf)
679             if buf == 'make install' or buf == 'make  install':
680                 line = '%make_install'
681         elif cmp_line == '%makeinstall':
682             line = '%make_install'
683         elif cmp_line == 'rm -rf %{buildroot}':
684             return
685
686         RpmSection.add(self, line)
687
688
689 #######################################################################
690
691
692 class RpmClean(RpmSection):
693     # if the section contains just rm -rf %{buildroot} then remove the whole section (including %clean)
694     pass
695
696
697 #######################################################################
698
699
700 class RpmScriptlets(RpmSection):
701     '''
702         Do %post -p /sbin/ldconfig when possible.
703     '''
704
705     def __init__(self):
706         RpmSection.__init__(self)
707         self.cache = []
708
709
710     def add(self, line):
711         if len(self.lines) == 0:
712             if not self.cache:
713                 if line.find(' -p ') == -1 and line.find(' -f ') == -1:
714                     self.cache.append(line)
715                     return
716             else:
717                 if line in ['', '/sbin/ldconfig' ]:
718                     self.cache.append(line)
719                     return
720                 else:
721                     for cached in self.cache:
722                         RpmSection.add(self, cached)
723                     self.cache = None
724
725         RpmSection.add(self, line)
726
727
728     def output(self, fout):
729         if self.cache:
730             RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig')
731             RpmSection.add(self, '')
732
733         RpmSection.output(self, fout)
734
735
736 #######################################################################
737
738
739 class RpmFiles(RpmSection):
740     """
741         Replace additional /usr, /etc and /var because we're sure we can use
742         macros there.
743
744         Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with
745         '%{_includedir}/mux/'
746     """
747
748     re_etcdir = re.compile('(^|\s)/etc/')
749     re_usrdir = re.compile('(^|\s)/usr/')
750     re_vardir = re.compile('(^|\s)/var/')
751
752     re_dir = re.compile('^\s*%dir\s*(\S+)\s*')
753
754     def __init__(self):
755         RpmSection.__init__(self)
756         self.dir_on_previous_line = None
757
758
759     def add(self, line):
760         line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line)
761         line = self.re_usrdir.sub(r'\1%{_prefix}/', line)
762         line = self.re_vardir.sub(r'\1%{_localstatedir}/', line)
763
764         if self.dir_on_previous_line:
765             if line == self.dir_on_previous_line + '/*':
766                 RpmSection.add(self, self.dir_on_previous_line + '/')
767                 self.dir_on_previous_line = None
768                 return
769             else:
770                 RpmSection.add(self, '%dir ' + self.dir_on_previous_line)
771                 self.dir_on_previous_line = None
772
773         match = self.re_dir.match(line)
774         if match:
775             self.dir_on_previous_line = match.group(1)
776             return
777
778         RpmSection.add(self, line)
779
780
781 #######################################################################
782
783
784 class RpmChangelog(RpmSection):
785     '''
786         Remove changelog entries.
787     '''
788
789     def add(self, line):
790         # only add the first line (%changelog)
791         if len(self.lines) == 0:
792             RpmSection.add(self, line)
793
794
795 #######################################################################
796
797
798 class RpmSpecCleaner:
799
800     specfile = None
801     fin = None
802     fout = None
803     current_section = None
804
805     re_spec_package = re.compile('^%package\s*', re.IGNORECASE)
806     re_spec_description = re.compile('^%description\s*', re.IGNORECASE)
807     re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE)
808     re_spec_build = re.compile('^%build\s*$', re.IGNORECASE)
809     re_spec_install = re.compile('^%install\s*$', re.IGNORECASE)
810     re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE)
811     re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE)
812     re_spec_files = re.compile('^%files\s*', re.IGNORECASE)
813     re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE)
814
815
816     section_starts = [
817         (re_spec_package, RpmPackage),
818         (re_spec_description, RpmDescription),
819         (re_spec_prep, RpmPrep),
820         (re_spec_build, RpmBuild),
821         (re_spec_install, RpmInstall),
822         (re_spec_clean, RpmClean),
823         (re_spec_scriptlets, RpmScriptlets),
824         (re_spec_files, RpmFiles),
825         (re_spec_changelog, RpmChangelog)
826     ]
827
828
829     def __init__(self, specfile, output, inline, force, diff, diff_prog):
830         if not specfile.endswith('.spec'):
831             raise RpmException('%s does not appear to be a spec file.' % specfile)
832
833         if not os.path.exists(specfile):
834             raise RpmException('%s does not exist.' % specfile)
835
836         self.specfile = specfile
837         self.output = output
838         self.inline = inline
839         self.diff = diff
840         self.diff_prog = diff_prog
841
842         self.fin = open(self.specfile)
843
844         if self.output:
845             if not force and os.path.exists(self.output):
846                 raise RpmException('%s already exists.' % self.output)
847             self.fout = open(self.output, 'w')
848         elif self.inline:
849             io = cStringIO.StringIO()
850             while True:
851                 bytes = self.fin.read(500 * 1024)
852                 if len(bytes) == 0:
853                     break
854                 io.write(bytes)
855
856             self.fin.close()
857             io.seek(0)
858             self.fin = io
859             self.fout = open(self.specfile, 'w')
860         elif self.diff:
861             self.fout = tempfile.NamedTemporaryFile(prefix=self.specfile)
862         else:
863             self.fout = sys.stdout
864
865
866     def run(self):
867         if not self.specfile or not self.fin:
868             raise RpmException('No spec file.')
869
870         def _line_for_new_section(self, line):
871             if isinstance(self.current_section, RpmCopyright):
872                 if not re_comment.match(line):
873                     return RpmPreamble
874
875             for (regexp, newclass) in self.section_starts:
876                 if regexp.match(line):
877                     return newclass
878
879             return None
880
881
882         self.current_section = RpmCopyright()
883
884         while True:
885             line = self.fin.readline()
886             if len(line) == 0:
887                 break
888             # Remove \n to make it easier to parse things
889             line = line[:-1]
890
891             new_class = _line_for_new_section(self, line)
892             if new_class:
893                 self.current_section.output(self.fout)
894                 self.current_section = new_class()
895
896             self.current_section.add(line)
897
898         self.current_section.output(self.fout)
899         self.fout.flush()
900
901         if self.diff:
902             cmd = shlex.split(self.diff_prog + " " + self.specfile.replace(" ","\\ ") + " " + self.fout.name.replace(" ","\\ "))
903             try:
904                 subprocess.call(cmd, shell=False)
905             except OSError as e:
906                 raise RpmException('Could not execute %s (%s)' % (self.diff_prog.split()[0], e.strerror))
907
908     def __del__(self):
909         if self.fin:
910             self.fin.close()
911             self.fin = None
912         if self.fout:
913             self.fout.close()
914             self.fout = None
915
916
917 #######################################################################
918
919
920 def main(args):
921     parser = optparse.OptionParser(epilog='This script cleans spec file according to some arbitrary style guide. The results it produces should always be checked by someone since it is not and will never be perfect.')
922
923     parser.add_option("-i", "--inline", action="store_true", dest="inline",
924                       default=False, help="edit the file inline")
925     parser.add_option("-o", "--output", dest="output",
926                       help="output file")
927     parser.add_option("-f", "--force", action="store_true", dest="force",
928                       default=False, help="overwrite output file if already existing")
929     parser.add_option("-d", "--diff", action="store_true", dest="diff",
930                       default=False, help="call external program to compare new and original specfile")
931     parser.add_option("--diff-prog", dest="diff_prog",
932                       help="program to generate diff (implies --diff)")
933     parser.add_option("-v", "--version", action="store_true", dest="version",
934                       default=False, help="display version (" + VERSION + ")")
935
936     (options, args) = parser.parse_args()
937
938     if options.version:
939         print 'spec-cleaner ' + VERSION
940         return 0
941
942     if len(args) != 1:
943         parser.print_help()
944         return 1
945
946     spec = os.path.expanduser(args[0])
947     if options.output:
948         options.output = os.path.expanduser(options.output)
949
950     if options.output == spec:
951         options.output = ''
952         options.inline = True
953
954     if options.diff_prog:
955         # --diff-prog implies -d
956         options.diff = True
957     else:
958         # if diff-prog is not specified, set default here
959         options.diff_prog = "vimdiff"
960
961     if options.output and options.inline:
962         print >> sys.stderr,  'Conflicting options: --inline and --output.'
963         return 1
964
965     if options.diff and options.output:
966         print >> sys.stderr,  'Conflicting options: --diff and --output.'
967         return 1
968
969     if options.diff and options.inline:
970         print >> sys.stderr,  'Conflicting options: --diff and --inline.'
971         return 1
972
973     try:
974         cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force, options.diff, options.diff_prog)
975         cleaner.run()
976     except RpmException, e:
977         print >> sys.stderr, '%s' % e
978         return 1
979
980     return 0
981
982 if __name__ == '__main__':
983     try:
984         res = main(sys.argv)
985         sys.exit(res)
986     except KeyboardInterrupt:
987         pass