Fix %package lines being moved while they shouldn't
[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|%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
547     def add(self, line):
548         if self.removing_authors:
549             return
550
551         if len(line) == 0:
552             if not self.previous_line or len(self.previous_line) == 0:
553                 return
554
555         if line == 'Authors:':
556             self.removing_authors = True
557             return
558
559         RpmSection.add(self, line)
560
561
562 #######################################################################
563
564
565 class RpmPrep(RpmSection):
566     '''
567         Try to simplify to %setup -q when possible.
568     '''
569
570     def add(self, line):
571         if line.startswith('%setup'):
572             cmp_line = line.replace(' -q', '')
573             cmp_line = cmp_line.replace(' -n %{name}-%{version}', '')
574             cmp_line = strip_useless_spaces(cmp_line)
575             if cmp_line == '%setup':
576                 line = '%setup -q'
577
578         RpmSection.add(self, line)
579
580
581 #######################################################################
582
583
584 class RpmBuild(RpmSection):
585     '''
586         Replace %{?_smp_mflags} fedora-ism with %{?jobs:-j%jobs}
587         Replace %{?jobs: -j%jobs} with %{?jobs:-j%jobs} (in some packages)
588     '''
589
590     def add(self, line):
591         if not re_comment.match(line):
592             line = line.replace('%{?_smp_mflags}', '%{?jobs:-j%jobs}')
593             line = line.replace(' %{?jobs: -j%jobs}', ' %{?jobs:-j%jobs}')
594
595         RpmSection.add(self, line)
596
597
598 #######################################################################
599
600
601 class RpmInstall(RpmSection):
602     '''
603         Remove commands that wipe out the build root.
604         Use %makeinstall macro.
605     '''
606
607     re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE)
608
609     def add(self, line):
610         # remove double spaces when comparing the line
611         cmp_line = strip_useless_spaces(line)
612         cmp_line = replace_buildroot(cmp_line)
613
614         if cmp_line.find('DESTDIR=%{buildroot}') != -1:
615             buf = cmp_line.replace('DESTDIR=%{buildroot}', '')
616             buf = strip_useless_spaces(buf)
617             if buf == 'make install':
618                 line = '%makeinstall'
619         elif cmp_line == 'rm -rf %{buildroot}':
620             return
621
622         if self.re_autoreqprov.match(line):
623             return
624
625         RpmSection.add(self, line)
626
627
628 #######################################################################
629
630
631 class RpmClean(RpmSection):
632     pass
633
634
635 #######################################################################
636
637
638 class RpmScriptlets(RpmSection):
639     '''
640         Do %post -p /sbin/ldconfig when possible.
641     '''
642
643     def __init__(self):
644         RpmSection.__init__(self)
645         self.cache = []
646
647
648     def add(self, line):
649         if len(self.lines) == 0:
650             if not self.cache:
651                 if line.find(' -p ') == -1 and line.find(' -f ') == -1:
652                     self.cache.append(line)
653                     return
654             else:
655                 if line in ['', '/sbin/ldconfig' ]:
656                     self.cache.append(line)
657                     return
658                 else:
659                     for cached in self.cache:
660                         RpmSection.add(self, cached)
661                     self.cache = None
662             
663         RpmSection.add(self, line)
664
665
666     def output(self, fout):
667         if self.cache:
668             RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig')
669             RpmSection.add(self, '')
670
671         RpmSection.output(self, fout)
672
673
674 #######################################################################
675
676
677 class RpmFiles(RpmSection):
678     '''
679         Replace additional /usr, /etc and /var because we're sure we can use
680         macros there.
681
682         Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with
683         '%{_includedir}/mux/'
684     '''
685
686     re_etcdir = re.compile('(^|\s)/etc/')
687     re_usrdir = re.compile('(^|\s)/usr/')
688     re_vardir = re.compile('(^|\s)/var/')
689
690     re_dir = re.compile('^\s*%dir\s*(\S+)\s*')
691
692     def __init__(self):
693         RpmSection.__init__(self)
694         self.dir_on_previous_line = None
695
696
697     def add(self, line):
698         line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line)
699         line = self.re_usrdir.sub(r'\1%{_prefix}/', line)
700         line = self.re_vardir.sub(r'\1%{_localstatedir}/', line)
701
702         if self.dir_on_previous_line:
703             if line == self.dir_on_previous_line + '/*':
704                 RpmSection.add(self, self.dir_on_previous_line + '/')
705                 self.dir_on_previous_line = None
706                 return
707             else:
708                 RpmSection.add(self, '%dir ' + self.dir_on_previous_line)
709                 self.dir_on_previous_line = None
710
711         match = self.re_dir.match(line)
712         if match:
713             self.dir_on_previous_line = match.group(1)
714             return
715
716         RpmSection.add(self, line)
717
718
719 #######################################################################
720
721
722 class RpmChangelog(RpmSection):
723     '''
724         Remove changelog entries.
725     '''
726
727     def add(self, line):
728         # only add the first line (%changelog)
729         if len(self.lines) == 0:
730             RpmSection.add(self, line)
731
732
733 #######################################################################
734
735
736 class RpmSpecCleaner:
737
738     specfile = None
739     fin = None
740     fout = None
741     current_section = None
742
743
744     re_spec_package = re.compile('^%package\s*', re.IGNORECASE)
745     re_spec_description = re.compile('^%description\s*', re.IGNORECASE)
746     re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE)
747     re_spec_build = re.compile('^%build\s*$', re.IGNORECASE)
748     re_spec_install = re.compile('^%install\s*$', re.IGNORECASE)
749     re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE)
750     re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE)
751     re_spec_files = re.compile('^%files\s*', re.IGNORECASE)
752     re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE)
753
754     section_starts = [
755         (re_spec_package, RpmPackage),
756         (re_spec_description, RpmDescription),
757         (re_spec_prep, RpmPrep),
758         (re_spec_build, RpmBuild),
759         (re_spec_install, RpmInstall),
760         (re_spec_clean, RpmClean),
761         (re_spec_scriptlets, RpmScriptlets),
762         (re_spec_files, RpmFiles),
763         (re_spec_changelog, RpmChangelog)
764     ]
765
766
767     def __init__(self, specfile, output, inline, force):
768         if not specfile.endswith('.spec'):
769             raise RpmException('%s does not appear to be a spec file.' % specfile)
770
771         if not os.path.exists(specfile):
772             raise RpmException('%s does not exist.' % specfile)
773
774         self.specfile = specfile
775         self.output = output
776         self.inline = inline
777
778         self.fin = open(self.specfile)
779
780         if self.output:
781             if not force and os.path.exists(self.output):
782                 raise RpmException('%s already exists.' % self.output)
783             self.fout = open(self.output, 'w')
784         elif self.inline:
785             io = cStringIO.StringIO()
786             while True:
787                 bytes = self.fin.read(500 * 1024)
788                 if len(bytes) == 0:
789                     break
790                 io.write(bytes)
791
792             self.fin.close()
793             io.seek(0)
794             self.fin = io
795             self.fout = open(self.specfile, 'w')
796         else:
797             self.fout = sys.stdout
798
799
800     def run(self):
801         if not self.specfile or not self.fin:
802             raise RpmException('No spec file.')
803
804         def _line_for_new_section(self, line):
805             if isinstance(self.current_section, RpmCopyright):
806                 if not re_comment.match(line):
807                     return RpmPreamble
808
809             for (regexp, newclass) in self.section_starts:
810                 if regexp.match(line):
811                     return newclass
812
813             return None
814
815
816         self.current_section = RpmCopyright()
817
818         while True:
819             line = self.fin.readline()
820             if len(line) == 0:
821                 break
822             # Remove \n to make it easier to parse things
823             line = line[:-1]
824
825             new_class = _line_for_new_section(self, line)
826             if new_class:
827                 self.current_section.output(self.fout)
828                 self.current_section = new_class()
829
830             self.current_section.add(line)
831
832         self.current_section.output(self.fout)
833
834
835     def __del__(self):
836         if self.fin:
837             self.fin.close()
838             self.fin = None
839         if self.fout:
840             self.fout.close()
841             self.fout = None
842
843
844 #######################################################################
845
846
847 def main(args):
848     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.')
849
850     parser.add_option("-i", "--inline", action="store_true", dest="inline",
851                       default=False, help="edit the file inline")
852     parser.add_option("-o", "--output", dest="output",
853                       help="output file")
854     parser.add_option("-f", "--force", action="store_true", dest="force",
855                       default=False, help="overwrite output file if already existing")
856
857     (options, args) = parser.parse_args()
858
859     if len(args) < 1:
860         print >> sys.stderr,  'Need a filename to work on.'
861         return 1
862     elif len(args) > 1:
863         print >> sys.stderr,  'Too many arguments.'
864         return 1
865
866     spec = args[0]
867
868     if options.output == spec:
869         options.output = ''
870         options.inline = True
871
872     if options.output and options.inline:
873         print >> sys.stderr,  'Conflicting options: --inline and --output.'
874         return 1
875
876     try:
877         cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force)
878         cleaner.run()
879     except RpmException, e:
880         print >> sys.stderr, '%s' % e
881         return 1
882
883     return 0
884
885 if __name__ == '__main__':
886     try:
887         res = main(sys.argv)
888         sys.exit(res)
889     except KeyboardInterrupt:
890         pass