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