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