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