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