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