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