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