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