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