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