Also replace plain %patch to %patch0
[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 def replace_macros(s):
151     for i in ['name', 'version', 'release']:
152         s = s.replace('%' + i, '%{' + i + '}')
153     return s
154
155 def replace_all(s):
156     s = replace_buildroot(s)
157     s = replace_optflags(s)
158     s = replace_known_dirs(s)
159     s = replace_remove_la(s)
160     s = replace_utils(s)
161     s = replace_buildservice(s)
162     s = replace_macros(s)
163     return s
164
165
166 #######################################################################
167
168
169 class RpmException(Exception):
170     pass
171
172
173 #######################################################################
174
175
176 class RpmSection(object):
177     '''
178         Basic cleanup: we remove trailing spaces.
179     '''
180
181     def __init__(self):
182         self.lines = []
183         self.previous_line = None
184
185     def add(self, line):
186         line = line.rstrip()
187         line = replace_all(line)
188         self.lines.append(line)
189         self.previous_line = line
190
191     def output(self, fout):
192         for line in self.lines:
193             fout.write(line + '\n')
194
195
196 #######################################################################
197
198
199 class RpmCopyright(RpmSection):
200     '''
201         Adds default copyright notice if needed.
202         Remove initial empty lines.
203         Remove norootforbuild.
204     '''
205
206
207     def _add_default_copyright(self):
208         self.lines.append(time.strftime('''#
209 # Please submit bugfixes or comments via http://bugs.opensuse.org/
210 #
211 '''))
212
213
214     def add(self, line):
215         if not self.lines and not line:
216             return
217
218         if line.startswith('# norootforbuild') or \
219            line.startswith('# usedforbuild'):
220             return
221
222         RpmSection.add(self, line)
223
224
225     def output(self, fout):
226         if not self.lines:
227             self._add_default_copyright()
228         RpmSection.output(self, fout)
229
230
231 #######################################################################
232
233
234 class RpmPreamble(RpmSection):
235     '''
236         Only keep one empty line for many consecutive ones.
237         Reorder lines.
238         Fix bad licenses.
239         Use one line per BuildRequires/Requires/etc.
240         Use %{version} instead of %{version}-%{release} for BuildRequires/etc.
241         Remove AutoReqProv.
242         Standardize BuildRoot.
243
244         This one is a bit tricky since we reorder things. We have a notion of
245         paragraphs, categories, and groups.
246
247         A paragraph is a list of non-empty lines. Conditional directives like
248         %if/%else/%endif also mark paragraphs. It contains categories.
249         A category is a list of lines on the same topic. It contains a list of
250         groups.
251         A group is a list of lines where the first few ones are either %define
252         or comment lines, and the last one is a normal line.
253
254         This means that the %define and comments will stay attached to one
255         line, even if we reorder the lines.
256     '''
257
258     re_if = re.compile('^\s*(?:%if\s|%ifarch\s|%ifnarch\s|%else\s*$|%endif\s*$)', re.IGNORECASE)
259
260     re_name = re.compile('^Name:\s*(\S*)', re.IGNORECASE)
261     re_version = re.compile('^Version:\s*(\S*)', re.IGNORECASE)
262     re_release = re.compile('^Release:\s*(\S*)', re.IGNORECASE)
263     re_license = re.compile('^License:\s*(.*)', re.IGNORECASE)
264     re_summary = re.compile('^Summary:\s*(.*)', re.IGNORECASE)
265     re_url = re.compile('^Url:\s*(\S*)', re.IGNORECASE)
266     re_group = re.compile('^Group:\s*(.*)', re.IGNORECASE)
267     re_source = re.compile('^Source(\d*):\s*(\S*)', re.IGNORECASE)
268     re_patch = re.compile('^((?:#[#\s]*)?)Patch(\d*):\s*(\S*)', re.IGNORECASE)
269     re_buildrequires = re.compile('^BuildRequires:\s*(.*)', re.IGNORECASE)
270     re_prereq = re.compile('^PreReq:\s*(.*)', re.IGNORECASE)
271     re_requires = re.compile('^Requires:\s*(.*)', re.IGNORECASE)
272     re_recommends = re.compile('^Recommends:\s*(.*)', re.IGNORECASE)
273     re_suggests = re.compile('^Suggests:\s*(.*)', re.IGNORECASE)
274     re_supplements = re.compile('^Supplements:\s*(.*)', re.IGNORECASE)
275     re_provides = re.compile('^Provides:\s*(.*)', re.IGNORECASE)
276     re_obsoletes = re.compile('^Obsoletes:\s*(.*)', re.IGNORECASE)
277     re_buildroot = re.compile('^\s*BuildRoot:', re.IGNORECASE)
278     re_buildarch = re.compile('^\s*BuildArch:\s*(.*)', re.IGNORECASE)
279
280     re_requires_token = re.compile('(\s*(\S+(?:\s*(?:[<>]=?|=)\s*[^\s,]+)?),?)')
281
282     category_to_re = {
283         'name': re_name,
284         'version': re_version,
285         'release': re_release,
286         'license': re_license,
287         'summary': re_summary,
288         'url': re_url,
289         'group': re_group,
290         # for source, we have a special match to keep the source number
291         # for patch, we have a special match to keep the patch number
292         'buildrequires': re_buildrequires,
293         'prereq': re_prereq,
294         'requires': re_requires,
295         'recommends': re_recommends,
296         'suggests': re_suggests,
297         'supplements': re_supplements,
298         # for provides/obsoletes, we have a special case because we group them
299         # for build root, we have a special match because we force its value
300         'buildarch': re_buildarch
301     }
302
303     category_to_key = {
304         'name': 'Name',
305         'version': 'Version',
306         'release': 'Release',
307         'license': 'License',
308         'summary': 'Summary',
309         'url': 'Url',
310         'group': 'Group',
311         'source': 'Source',
312         'patch': 'Patch',
313         'buildrequires': 'BuildRequires',
314         'prereq': 'PreReq',
315         'requires': 'Requires',
316         'recommends': 'Recommends',
317         'suggests': 'Suggests',
318         'supplements': 'Supplements',
319         # Provides/Obsoletes cannot be part of this since we want to keep them
320         # mixed, so we'll have to specify the key when needed
321         'buildroot': 'BuildRoot',
322         'buildarch': 'BuildArch'
323     }
324
325     category_to_fixer = {
326     }
327
328     license_fixes = {
329         'LGPL v2.0 only': 'LGPLv2.0',
330         'LGPL v2.0 or later': 'LGPLv2.0+',
331         'LGPL v2.1 only': 'LGPLv2.1',
332         'LGPL v2.1 or later': 'LGPLv2.1+',
333         'LGPL v3 only': 'LGPLv3',
334         'LGPL v3 or later': 'LGPLv3+',
335         'GPL v2 only': 'GPLv2',
336         'GPL v2 or later': 'GPLv2+',
337         'GPL v3 only': 'GPLv3',
338         'GPL v3 or later': 'GPLv3+'
339     }
340
341     categories_order = [ 'name', 'version', 'release', 'license', 'summary', 'url', 'group', 'source', 'patch', 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements', 'provides_obsoletes', 'buildroot', 'buildarch', 'misc' ]
342
343     categories_with_sorted_package_tokens = [ 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements' ]
344     categories_with_package_tokens = categories_with_sorted_package_tokens[:]
345     categories_with_package_tokens.append('provides_obsoletes')
346
347     re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE)
348
349
350     def __init__(self):
351         RpmSection.__init__(self)
352         self._start_paragraph()
353
354
355     def _start_paragraph(self):
356         self.paragraph = {}
357         for i in self.categories_order:
358             self.paragraph[i] = []
359         self.current_group = []
360
361
362     def _add_group(self, group):
363         t = type(group)
364
365         if t == str:
366             RpmSection.add(self, group)
367         elif t == list:
368             for subgroup in group:
369                 self._add_group(subgroup)
370         else:
371             raise RpmException('Unknown type of group in preamble: %s' % t)
372
373
374     def _end_paragraph(self):
375         def sort_helper_key(a):
376             t = type(a)
377             if t == str:
378                 return a
379             elif t == list:
380                 return a[-1]
381             else:
382                 raise RpmException('Unknown type during sort: %s' % t)
383
384         for i in self.categories_order:
385             if i in self.categories_with_sorted_package_tokens:
386                 self.paragraph[i].sort(key=sort_helper_key)
387             for group in self.paragraph[i]:
388                 self._add_group(group)
389         if self.current_group:
390             # the current group was not added to any category. It's just some
391             # random stuff that should be at the end anyway.
392             self._add_group(self.current_group)
393
394         self._start_paragraph()
395
396
397     def _fix_license(self, value):
398         licenses = value.split(';')
399         for (index, license) in enumerate(licenses):
400             license = strip_useless_spaces(license)
401             if self.license_fixes.has_key(license):
402                 license = self.license_fixes[license]
403             licenses[index] = license
404
405         return [ ' ; '.join(licenses) ]
406
407     category_to_fixer['license'] = _fix_license
408
409
410     def _pkgname_to_pkgconfig(self, value):
411         r = {
412           'cairo-devel': 'cairo',
413           'dbus-1-devel': 'dbus-1',
414           'dbus-1-glib-devel': 'dbus-glib-1',
415           'gconf2-devel': 'gconf-2.0',
416           'exo-devel': 'exo-1',
417           'glib2-devel': 'glib-2.0',
418           'gtk2-devel': 'gtk+-2.0',
419           'hal-devel': 'hal',
420           'libexif-devel': 'libexif',
421           'libgarcon-devel': 'garcon-1',
422           'libglade2-devel': 'libglade-2.0',
423           'libgladeui-1_0-devel': 'gladeui-1.0',
424           'libgudev-1_0-devel': 'gudev-1.0',
425           'libnotify-devel': 'libnotify',
426           'libwnck-devel': 'libwnck-1.0',
427           'libxfce4ui-devel': 'libxfce4ui-1',
428           'libxfce4util-devel': 'libxfce4util-1.0',
429           'libxfcegui4-devel': 'libxfcegui4-1.0',
430           'libxfconf-devel': 'libxfconf-0',
431           'libxklavier-devel': 'libxklavier',
432           'libxml2-devel': 'libxml-2.0',
433           'startup-notification-devel': 'libstartup-notification-1.0',
434           'xfce4-panel-devel': 'libxfce4panel-1.0',
435         }
436         for i in r:
437             value = value.replace(i, 'pkgconfig('+r[i]+')')
438         return value
439
440     def _fix_list_of_packages(self, value):
441         if self.re_requires_token.match(value):
442             tokens = [ item[1] for item in self.re_requires_token.findall(value) ]
443             for (index, token) in enumerate(tokens):
444                 token = token.replace('%{version}-%{release}', '%{version}')
445                 token = token.replace(' ','')
446                 token = re.sub(r'([<>]=?|=)', r' \1 ', token)
447                 token = self._pkgname_to_pkgconfig(token)
448                 tokens[index] = token
449
450             tokens.sort()
451             return tokens
452         else:
453             return [ value ]
454
455     for i in categories_with_package_tokens:
456         category_to_fixer[i] = _fix_list_of_packages
457
458
459     def _add_line_value_to(self, category, value, key = None):
460         """
461             Change a key-value line, to make sure we have the right spacing.
462
463             Note: since we don't have a key <-> category matching, we need to
464             redo one. (Eg: Provides and Obsoletes are in the same category)
465         """
466         keylen = len('BuildRequires:  ')
467
468         if key:
469             pass
470         elif self.category_to_key.has_key(category):
471             key = self.category_to_key[category]
472         else:
473             raise RpmException('Unhandled category in preamble: %s' % category)
474
475         key += ':'
476         while len(key) < keylen:
477             key += ' '
478
479         if self.category_to_fixer.has_key(category):
480             values = self.category_to_fixer[category](self, value)
481         else:
482             values = [ value ]
483
484         for value in values:
485             line = key + value
486             self._add_line_to(category, line)
487
488
489     def _add_line_to(self, category, line):
490         if self.current_group:
491             self.current_group.append(line)
492             self.paragraph[category].append(self.current_group)
493             self.current_group = []
494         else:
495             self.paragraph[category].append(line)
496
497         self.previous_line = line
498
499
500     def add(self, line):
501         if len(line) == 0:
502             if not self.previous_line or len(self.previous_line) == 0:
503                 return
504
505             # we put the empty line in the current group (so we don't list it),
506             # and write the paragraph
507             self.current_group.append(line)
508             self._end_paragraph()
509             self.previous_line = line
510             return
511
512         elif self.re_if.match(line):
513             # %if/%else/%endif marks the end of the previous paragraph
514             # We append the line at the end of the previous paragraph, though,
515             # since it will stay at the end there. If putting it at the
516             # beginning of the next paragraph, it will likely move (with the
517             # misc category).
518             self.current_group.append(line)
519             self._end_paragraph()
520             self.previous_line = line
521             return
522
523         elif re_comment.match(line) or re_define.match(line):
524             self.current_group.append(line)
525             self.previous_line = line
526             return
527
528         elif self.re_autoreqprov.match(line):
529             return
530
531         elif self.re_source.match(line):
532             match = self.re_source.match(line)
533             self._add_line_value_to('source', match.group(2), key = 'Source%s' % match.group(1))
534             return
535
536         elif self.re_patch.match(line):
537             # FIXME: this is not perfect, but it's good enough for most cases
538             if not self.previous_line or not re_comment.match(self.previous_line):
539                 self.current_group.append('# PATCH-MISSING-TAG -- See http://en.opensuse.org/Packaging/Patches')
540
541             match = self.re_patch.match(line)
542             # convert Patch: to Patch0:
543             if match.group(2) == '':
544                 zero = '0'
545             else:
546                 zero = ''
547             self._add_line_value_to('source', match.group(3), key = '%sPatch%s%s' % (match.group(1), zero, match.group(2)))
548             return
549
550         elif self.re_provides.match(line):
551             match = self.re_provides.match(line)
552             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Provides')
553             return
554
555         elif self.re_obsoletes.match(line):
556             match = self.re_obsoletes.match(line)
557             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Obsoletes')
558             return
559
560         elif self.re_buildroot.match(line):
561             if len(self.paragraph['buildroot']) == 0:
562                 self._add_line_value_to('buildroot', '%{_tmppath}/%{name}-%{version}-build')
563             return
564
565         else:
566             for (category, regexp) in self.category_to_re.iteritems():
567                 match = regexp.match(line)
568                 if match:
569                     self._add_line_value_to(category, match.group(1))
570                     return
571
572             self._add_line_to('misc', line)
573
574
575     def output(self, fout):
576         self._end_paragraph()
577         RpmSection.output(self, fout)
578
579
580 #######################################################################
581
582
583 class RpmPackage(RpmPreamble):
584     '''
585         We handle this the same was as the preamble.
586     '''
587
588     def add(self, line):
589         # The first line (%package) should always be added and is different
590         # from the lines we handle in RpmPreamble.
591         if self.previous_line is None:
592             RpmSection.add(self, line)
593             return
594
595         RpmPreamble.add(self, line)
596
597
598 #######################################################################
599
600
601 class RpmDescription(RpmSection):
602     '''
603         Only keep one empty line for many consecutive ones.
604         Remove Authors from description.
605     '''
606
607     def __init__(self):
608         RpmSection.__init__(self)
609         self.removing_authors = False
610         # Tracks the use of a macro. When this happens and we're still in a
611         # description, we actually don't know where we are so we just put all
612         # the following lines blindly, without trying to fix anything.
613         self.unknown_line = False
614
615     def add(self, line):
616         lstrip = line.lstrip()
617         if self.previous_line != None and len(lstrip) > 0 and lstrip[0] == '%':
618             self.unknown_line = True
619
620         if self.removing_authors and not self.unknown_line:
621             return
622
623         if len(line) == 0:
624             if not self.previous_line or len(self.previous_line) == 0:
625                 return
626
627         if line == 'Authors:':
628             self.removing_authors = True
629             return
630
631         RpmSection.add(self, line)
632
633
634 #######################################################################
635
636
637 class RpmPrep(RpmSection):
638     '''
639         Try to simplify to %setup -q when possible.
640         Replace %patch with %patch0
641     '''
642
643     def add(self, line):
644         if line.startswith('%setup'):
645             cmp_line = line.replace(' -q', '')
646             cmp_line = cmp_line.replace(' -n %{name}-%{version}', '')
647             cmp_line = strip_useless_spaces(cmp_line)
648             if cmp_line == '%setup':
649                 line = '%setup -q'
650         if line.startswith('%patch ') or line == '%patch':
651             line = line.replace('%patch','%patch0')
652
653         RpmSection.add(self, line)
654
655
656 #######################################################################
657
658
659 class RpmBuild(RpmSection):
660     '''
661         Replace %{?jobs:-j%jobs} (suse-ism) with %{?_smp_mflags}
662     '''
663
664     def add(self, line):
665         if not re_comment.match(line):
666             line = line.replace('%_smp_mflags'     , '%{?_smp_mflags}')
667             line = line.replace('%{_smp_mflags}'   , '%{?_smp_mflags}')
668             line = line.replace('%{?jobs:-j%jobs}' , '%{?_smp_mflags}')
669             line = line.replace('%{?jobs: -j%jobs}', '%{?_smp_mflags}')
670             line = line.replace('%{?jobs:-j %jobs}', '%{?_smp_mflags}')
671
672         RpmSection.add(self, line)
673
674
675 #######################################################################
676
677
678 class RpmInstall(RpmSection):
679     '''
680         Remove commands that wipe out the build root.
681         Use %make_install macro.
682         Replace %makeinstall (suse-ism).
683     '''
684
685     def add(self, line):
686         # remove double spaces when comparing the line
687         cmp_line = strip_useless_spaces(line)
688         cmp_line = replace_buildroot(cmp_line)
689
690         if cmp_line.find('DESTDIR=%{buildroot}') != -1:
691             buf = cmp_line.replace('DESTDIR=%{buildroot}', '')
692             buf = strip_useless_spaces(buf)
693             if buf == 'make install' or buf == 'make  install':
694                 line = '%make_install'
695         elif cmp_line == '%makeinstall':
696             line = '%make_install'
697         elif cmp_line == 'rm -rf %{buildroot}':
698             return
699
700         RpmSection.add(self, line)
701
702
703 #######################################################################
704
705
706 class RpmClean(RpmSection):
707     # if the section contains just rm -rf %{buildroot} then remove the whole section (including %clean)
708     pass
709
710
711 #######################################################################
712
713
714 class RpmScriptlets(RpmSection):
715     '''
716         Do %post -p /sbin/ldconfig when possible.
717     '''
718
719     def __init__(self):
720         RpmSection.__init__(self)
721         self.cache = []
722
723
724     def add(self, line):
725         if len(self.lines) == 0:
726             if not self.cache:
727                 if line.find(' -p ') == -1 and line.find(' -f ') == -1:
728                     self.cache.append(line)
729                     return
730             else:
731                 if line in ['', '/sbin/ldconfig' ]:
732                     self.cache.append(line)
733                     return
734                 else:
735                     for cached in self.cache:
736                         RpmSection.add(self, cached)
737                     self.cache = None
738
739         RpmSection.add(self, line)
740
741
742     def output(self, fout):
743         if self.cache:
744             RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig')
745             RpmSection.add(self, '')
746
747         RpmSection.output(self, fout)
748
749
750 #######################################################################
751
752
753 class RpmFiles(RpmSection):
754     """
755         Replace additional /usr, /etc and /var because we're sure we can use
756         macros there.
757
758         Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with
759         '%{_includedir}/mux/'
760     """
761
762     re_etcdir = re.compile('(^|\s)/etc/')
763     re_usrdir = re.compile('(^|\s)/usr/')
764     re_vardir = re.compile('(^|\s)/var/')
765
766     re_dir = re.compile('^\s*%dir\s*(\S+)\s*')
767
768     def __init__(self):
769         RpmSection.__init__(self)
770         self.dir_on_previous_line = None
771
772
773     def add(self, line):
774         line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line)
775         line = self.re_usrdir.sub(r'\1%{_prefix}/', line)
776         line = self.re_vardir.sub(r'\1%{_localstatedir}/', line)
777
778         if self.dir_on_previous_line:
779             if line == self.dir_on_previous_line + '/*':
780                 RpmSection.add(self, self.dir_on_previous_line + '/')
781                 self.dir_on_previous_line = None
782                 return
783             else:
784                 RpmSection.add(self, '%dir ' + self.dir_on_previous_line)
785                 self.dir_on_previous_line = None
786
787         match = self.re_dir.match(line)
788         if match:
789             self.dir_on_previous_line = match.group(1)
790             return
791
792         RpmSection.add(self, line)
793
794
795 #######################################################################
796
797
798 class RpmChangelog(RpmSection):
799     '''
800         Remove changelog entries.
801     '''
802
803     def add(self, line):
804         # only add the first line (%changelog)
805         if len(self.lines) == 0:
806             RpmSection.add(self, line)
807
808
809 #######################################################################
810
811
812 class RpmSpecCleaner:
813
814     specfile = None
815     fin = None
816     fout = None
817     current_section = None
818
819     re_spec_package = re.compile('^%package\s*', re.IGNORECASE)
820     re_spec_description = re.compile('^%description\s*', re.IGNORECASE)
821     re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE)
822     re_spec_build = re.compile('^%build\s*$', re.IGNORECASE)
823     re_spec_install = re.compile('^%install\s*$', re.IGNORECASE)
824     re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE)
825     re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE)
826     re_spec_files = re.compile('^%files\s*', re.IGNORECASE)
827     re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE)
828
829
830     section_starts = [
831         (re_spec_package, RpmPackage),
832         (re_spec_description, RpmDescription),
833         (re_spec_prep, RpmPrep),
834         (re_spec_build, RpmBuild),
835         (re_spec_install, RpmInstall),
836         (re_spec_clean, RpmClean),
837         (re_spec_scriptlets, RpmScriptlets),
838         (re_spec_files, RpmFiles),
839         (re_spec_changelog, RpmChangelog)
840     ]
841
842
843     def __init__(self, specfile, output, inline, force, diff, diff_prog):
844         if not specfile.endswith('.spec'):
845             raise RpmException('%s does not appear to be a spec file.' % specfile)
846
847         if not os.path.exists(specfile):
848             raise RpmException('%s does not exist.' % specfile)
849
850         self.specfile = specfile
851         self.output = output
852         self.inline = inline
853         self.diff = diff
854         self.diff_prog = diff_prog
855
856         self.fin = open(self.specfile)
857
858         if self.output:
859             if not force and os.path.exists(self.output):
860                 raise RpmException('%s already exists.' % self.output)
861             self.fout = open(self.output, 'w')
862         elif self.inline:
863             io = cStringIO.StringIO()
864             while True:
865                 bytes = self.fin.read(500 * 1024)
866                 if len(bytes) == 0:
867                     break
868                 io.write(bytes)
869
870             self.fin.close()
871             io.seek(0)
872             self.fin = io
873             self.fout = open(self.specfile, 'w')
874         elif self.diff:
875             self.fout = tempfile.NamedTemporaryFile(prefix=self.specfile)
876         else:
877             self.fout = sys.stdout
878
879
880     def run(self):
881         if not self.specfile or not self.fin:
882             raise RpmException('No spec file.')
883
884         def _line_for_new_section(self, line):
885             if isinstance(self.current_section, RpmCopyright):
886                 if not re_comment.match(line):
887                     return RpmPreamble
888
889             for (regexp, newclass) in self.section_starts:
890                 if regexp.match(line):
891                     return newclass
892
893             return None
894
895
896         self.current_section = RpmCopyright()
897
898         while True:
899             line = self.fin.readline()
900             if len(line) == 0:
901                 break
902             # Remove \n to make it easier to parse things
903             line = line[:-1]
904
905             new_class = _line_for_new_section(self, line)
906             if new_class:
907                 self.current_section.output(self.fout)
908                 self.current_section = new_class()
909
910             self.current_section.add(line)
911
912         self.current_section.output(self.fout)
913         self.fout.flush()
914
915         if self.diff:
916             cmd = shlex.split(self.diff_prog + " " + self.specfile.replace(" ","\\ ") + " " + self.fout.name.replace(" ","\\ "))
917             try:
918                 subprocess.call(cmd, shell=False)
919             except OSError as e:
920                 raise RpmException('Could not execute %s (%s)' % (self.diff_prog.split()[0], e.strerror))
921
922     def __del__(self):
923         if self.fin:
924             self.fin.close()
925             self.fin = None
926         if self.fout:
927             self.fout.close()
928             self.fout = None
929
930
931 #######################################################################
932
933
934 def main(args):
935     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.')
936
937     parser.add_option("-i", "--inline", action="store_true", dest="inline",
938                       default=False, help="edit the file inline")
939     parser.add_option("-o", "--output", dest="output",
940                       help="output file")
941     parser.add_option("-f", "--force", action="store_true", dest="force",
942                       default=False, help="overwrite output file if already existing")
943     parser.add_option("-d", "--diff", action="store_true", dest="diff",
944                       default=False, help="call external program to compare new and original specfile")
945     parser.add_option("--diff-prog", dest="diff_prog",
946                       help="program to generate diff (implies --diff)")
947     parser.add_option("-v", "--version", action="store_true", dest="version",
948                       default=False, help="display version (" + VERSION + ")")
949
950     (options, args) = parser.parse_args()
951
952     if options.version:
953         print 'spec-cleaner ' + VERSION
954         return 0
955
956     if len(args) != 1:
957         parser.print_help()
958         return 1
959
960     spec = os.path.expanduser(args[0])
961     if options.output:
962         options.output = os.path.expanduser(options.output)
963
964     if options.output == spec:
965         options.output = ''
966         options.inline = True
967
968     if options.diff_prog:
969         # --diff-prog implies -d
970         options.diff = True
971     else:
972         # if diff-prog is not specified, set default here
973         options.diff_prog = "vimdiff"
974
975     if options.output and options.inline:
976         print >> sys.stderr,  'Conflicting options: --inline and --output.'
977         return 1
978
979     if options.diff and options.output:
980         print >> sys.stderr,  'Conflicting options: --diff and --output.'
981         return 1
982
983     if options.diff and options.inline:
984         print >> sys.stderr,  'Conflicting options: --diff and --inline.'
985         return 1
986
987     try:
988         cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force, options.diff, options.diff_prog)
989         cleaner.run()
990     except RpmException, e:
991         print >> sys.stderr, '%s' % e
992         return 1
993
994     return 0
995
996 if __name__ == '__main__':
997     try:
998         res = main(sys.argv)
999         sys.exit(res)
1000     except KeyboardInterrupt:
1001         pass