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