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