start replacing requirements for pkgconfig() symbols
[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 _pkgname_to_pkgconfig(self, value):
402         r = {
403           'dbus-1-devel': 'dbus-1',
404           'dbus-1-glib-devel': 'dbus-glib-1',
405           'glib2-devel': 'glib-2.0',
406           'gtk2-devel': 'gtk-2.0',
407         }
408         for i in r:
409             value = value.replace(i, 'pkgconfig('+r[i]+')')
410         return value
411
412     def _fix_list_of_packages(self, value):
413         if self.re_requires_token.match(value):
414             tokens = [ item[1] for item in self.re_requires_token.findall(value) ]
415             for (index, token) in enumerate(tokens):
416                 token = token.replace('%{version}-%{release}', '%{version}')
417                 token = token.replace(' ','')
418                 token = re.sub(r'([<>]=?|=)', r' \1 ', token)
419                 token = self._pkgname_to_pkgconfig(token)
420                 tokens[index] = token
421
422             tokens.sort()
423             return tokens
424         else:
425             return [ value ]
426
427     for i in categories_with_package_tokens:
428         category_to_fixer[i] = _fix_list_of_packages
429
430
431     def _add_line_value_to(self, category, value, key = None):
432         """
433             Change a key-value line, to make sure we have the right spacing.
434
435             Note: since we don't have a key <-> category matching, we need to
436             redo one. (Eg: Provides and Obsoletes are in the same category)
437         """
438         keylen = len('BuildRequires:  ')
439
440         if key:
441             pass
442         elif self.category_to_key.has_key(category):
443             key = self.category_to_key[category]
444         else:
445             raise RpmException('Unhandled category in preamble: %s' % category)
446
447         key += ':'
448         while len(key) < keylen:
449             key += ' '
450
451         if self.category_to_fixer.has_key(category):
452             values = self.category_to_fixer[category](self, value)
453         else:
454             values = [ value ]
455
456         for value in values:
457             line = key + value
458             self._add_line_to(category, line)
459
460
461     def _add_line_to(self, category, line):
462         if self.current_group:
463             self.current_group.append(line)
464             self.paragraph[category].append(self.current_group)
465             self.current_group = []
466         else:
467             self.paragraph[category].append(line)
468
469         self.previous_line = line
470
471
472     def add(self, line):
473         if len(line) == 0:
474             if not self.previous_line or len(self.previous_line) == 0:
475                 return
476
477             # we put the empty line in the current group (so we don't list it),
478             # and write the paragraph
479             self.current_group.append(line)
480             self._end_paragraph()
481             self.previous_line = line
482             return
483
484         elif self.re_if.match(line):
485             # %if/%else/%endif marks the end of the previous paragraph
486             # We append the line at the end of the previous paragraph, though,
487             # since it will stay at the end there. If putting it at the
488             # beginning of the next paragraph, it will likely move (with the
489             # misc category).
490             self.current_group.append(line)
491             self._end_paragraph()
492             self.previous_line = line
493             return
494
495         elif re_comment.match(line) or re_define.match(line):
496             self.current_group.append(line)
497             self.previous_line = line
498             return
499
500         elif self.re_autoreqprov.match(line):
501             return
502
503         elif self.re_source.match(line):
504             match = self.re_source.match(line)
505             self._add_line_value_to('source', match.group(2), key = 'Source%s' % match.group(1))
506             return
507
508         elif self.re_patch.match(line):
509             # FIXME: this is not perfect, but it's good enough for most cases
510             if not self.previous_line or not re_comment.match(self.previous_line):
511                 self.current_group.append('# PATCH-MISSING-TAG -- See http://en.opensuse.org/Packaging/Patches')
512
513             match = self.re_patch.match(line)
514             # convert Patch: to Patch0:
515             if match.group(2) == '':
516                 zero = '0'
517             else:
518                 zero = ''
519             self._add_line_value_to('source', match.group(3), key = '%sPatch%s%s' % (match.group(1), zero, match.group(2)))
520             return
521
522         elif self.re_provides.match(line):
523             match = self.re_provides.match(line)
524             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Provides')
525             return
526
527         elif self.re_obsoletes.match(line):
528             match = self.re_obsoletes.match(line)
529             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Obsoletes')
530             return
531
532         elif self.re_buildroot.match(line):
533             if len(self.paragraph['buildroot']) == 0:
534                 self._add_line_value_to('buildroot', '%{_tmppath}/%{name}-%{version}-build')
535             return
536
537         else:
538             for (category, regexp) in self.category_to_re.iteritems():
539                 match = regexp.match(line)
540                 if match:
541                     self._add_line_value_to(category, match.group(1))
542                     return
543
544             self._add_line_to('misc', line)
545
546
547     def output(self, fout):
548         self._end_paragraph()
549         RpmSection.output(self, fout)
550
551
552 #######################################################################
553
554
555 class RpmPackage(RpmPreamble):
556     '''
557         We handle this the same was as the preamble.
558     '''
559
560     def add(self, line):
561         # The first line (%package) should always be added and is different
562         # from the lines we handle in RpmPreamble.
563         if self.previous_line is None:
564             RpmSection.add(self, line)
565             return
566
567         RpmPreamble.add(self, line)
568
569
570 #######################################################################
571
572
573 class RpmDescription(RpmSection):
574     '''
575         Only keep one empty line for many consecutive ones.
576         Remove Authors from description.
577     '''
578
579     def __init__(self):
580         RpmSection.__init__(self)
581         self.removing_authors = False
582         # Tracks the use of a macro. When this happens and we're still in a
583         # description, we actually don't know where we are so we just put all
584         # the following lines blindly, without trying to fix anything.
585         self.unknown_line = False
586
587     def add(self, line):
588         lstrip = line.lstrip()
589         if self.previous_line != None and len(lstrip) > 0 and lstrip[0] == '%':
590             self.unknown_line = True
591
592         if self.removing_authors and not self.unknown_line:
593             return
594
595         if len(line) == 0:
596             if not self.previous_line or len(self.previous_line) == 0:
597                 return
598
599         if line == 'Authors:':
600             self.removing_authors = True
601             return
602
603         RpmSection.add(self, line)
604
605
606 #######################################################################
607
608
609 class RpmPrep(RpmSection):
610     '''
611         Try to simplify to %setup -q when possible.
612         Replace %patch with %patch0
613     '''
614
615     def add(self, line):
616         if line.startswith('%setup'):
617             cmp_line = line.replace(' -q', '')
618             cmp_line = cmp_line.replace(' -n %{name}-%{version}', '')
619             cmp_line = strip_useless_spaces(cmp_line)
620             if cmp_line == '%setup':
621                 line = '%setup -q'
622         if line.startswith('%patch '):
623             line = line.replace('%patch','%patch0')
624
625         RpmSection.add(self, line)
626
627
628 #######################################################################
629
630
631 class RpmBuild(RpmSection):
632     '''
633         Replace %{?jobs:-j%jobs} (suse-ism) with %{?_smp_mflags}
634     '''
635
636     def add(self, line):
637         if not re_comment.match(line):
638             line = line.replace('%_smp_mflags'     , '%{?_smp_mflags}')
639             line = line.replace('%{_smp_mflags}'   , '%{?_smp_mflags}')
640             line = line.replace('%{?jobs:-j%jobs}' , '%{?_smp_mflags}')
641             line = line.replace('%{?jobs: -j%jobs}', '%{?_smp_mflags}')
642             line = line.replace('%{?jobs:-j %jobs}', '%{?_smp_mflags}')
643
644         RpmSection.add(self, line)
645
646
647 #######################################################################
648
649
650 class RpmInstall(RpmSection):
651     '''
652         Remove commands that wipe out the build root.
653         Use %make_install macro.
654         Replace %makeinstall (suse-ism).
655     '''
656
657     def add(self, line):
658         # remove double spaces when comparing the line
659         cmp_line = strip_useless_spaces(line)
660         cmp_line = replace_buildroot(cmp_line)
661
662         if cmp_line.find('DESTDIR=%{buildroot}') != -1:
663             buf = cmp_line.replace('DESTDIR=%{buildroot}', '')
664             buf = strip_useless_spaces(buf)
665             if buf == 'make install' or buf == 'make  install':
666                 line = '%make_install'
667         elif cmp_line == '%makeinstall':
668             line = '%make_install'
669         elif cmp_line == 'rm -rf %{buildroot}':
670             return
671
672         RpmSection.add(self, line)
673
674
675 #######################################################################
676
677
678 class RpmClean(RpmSection):
679     # if the section contains just rm -rf %{buildroot} then remove the whole section (including %clean)
680     pass
681
682
683 #######################################################################
684
685
686 class RpmScriptlets(RpmSection):
687     '''
688         Do %post -p /sbin/ldconfig when possible.
689     '''
690
691     def __init__(self):
692         RpmSection.__init__(self)
693         self.cache = []
694
695
696     def add(self, line):
697         if len(self.lines) == 0:
698             if not self.cache:
699                 if line.find(' -p ') == -1 and line.find(' -f ') == -1:
700                     self.cache.append(line)
701                     return
702             else:
703                 if line in ['', '/sbin/ldconfig' ]:
704                     self.cache.append(line)
705                     return
706                 else:
707                     for cached in self.cache:
708                         RpmSection.add(self, cached)
709                     self.cache = None
710
711         RpmSection.add(self, line)
712
713
714     def output(self, fout):
715         if self.cache:
716             RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig')
717             RpmSection.add(self, '')
718
719         RpmSection.output(self, fout)
720
721
722 #######################################################################
723
724
725 class RpmFiles(RpmSection):
726     """
727         Replace additional /usr, /etc and /var because we're sure we can use
728         macros there.
729
730         Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with
731         '%{_includedir}/mux/'
732     """
733
734     re_etcdir = re.compile('(^|\s)/etc/')
735     re_usrdir = re.compile('(^|\s)/usr/')
736     re_vardir = re.compile('(^|\s)/var/')
737
738     re_dir = re.compile('^\s*%dir\s*(\S+)\s*')
739
740     def __init__(self):
741         RpmSection.__init__(self)
742         self.dir_on_previous_line = None
743
744
745     def add(self, line):
746         line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line)
747         line = self.re_usrdir.sub(r'\1%{_prefix}/', line)
748         line = self.re_vardir.sub(r'\1%{_localstatedir}/', line)
749
750         if self.dir_on_previous_line:
751             if line == self.dir_on_previous_line + '/*':
752                 RpmSection.add(self, self.dir_on_previous_line + '/')
753                 self.dir_on_previous_line = None
754                 return
755             else:
756                 RpmSection.add(self, '%dir ' + self.dir_on_previous_line)
757                 self.dir_on_previous_line = None
758
759         match = self.re_dir.match(line)
760         if match:
761             self.dir_on_previous_line = match.group(1)
762             return
763
764         RpmSection.add(self, line)
765
766
767 #######################################################################
768
769
770 class RpmChangelog(RpmSection):
771     '''
772         Remove changelog entries.
773     '''
774
775     def add(self, line):
776         # only add the first line (%changelog)
777         if len(self.lines) == 0:
778             RpmSection.add(self, line)
779
780
781 #######################################################################
782
783
784 class RpmSpecCleaner:
785
786     specfile = None
787     fin = None
788     fout = None
789     current_section = None
790
791
792     re_spec_package = re.compile('^%package\s*', re.IGNORECASE)
793     re_spec_description = re.compile('^%description\s*', re.IGNORECASE)
794     re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE)
795     re_spec_build = re.compile('^%build\s*$', re.IGNORECASE)
796     re_spec_install = re.compile('^%install\s*$', re.IGNORECASE)
797     re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE)
798     re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE)
799     re_spec_files = re.compile('^%files\s*', re.IGNORECASE)
800     re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE)
801
802
803     section_starts = [
804         (re_spec_package, RpmPackage),
805         (re_spec_description, RpmDescription),
806         (re_spec_prep, RpmPrep),
807         (re_spec_build, RpmBuild),
808         (re_spec_install, RpmInstall),
809         (re_spec_clean, RpmClean),
810         (re_spec_scriptlets, RpmScriptlets),
811         (re_spec_files, RpmFiles),
812         (re_spec_changelog, RpmChangelog)
813     ]
814
815
816     def __init__(self, specfile, output, inline, force):
817         if not specfile.endswith('.spec'):
818             raise RpmException('%s does not appear to be a spec file.' % specfile)
819
820         if not os.path.exists(specfile):
821             raise RpmException('%s does not exist.' % specfile)
822
823         self.specfile = specfile
824         self.output = output
825         self.inline = inline
826
827         self.fin = open(self.specfile)
828
829         if self.output:
830             if not force and os.path.exists(self.output):
831                 raise RpmException('%s already exists.' % self.output)
832             self.fout = open(self.output, 'w')
833         elif self.inline:
834             io = cStringIO.StringIO()
835             while True:
836                 bytes = self.fin.read(500 * 1024)
837                 if len(bytes) == 0:
838                     break
839                 io.write(bytes)
840
841             self.fin.close()
842             io.seek(0)
843             self.fin = io
844             self.fout = open(self.specfile, 'w')
845         else:
846             self.fout = sys.stdout
847
848
849     def run(self):
850         if not self.specfile or not self.fin:
851             raise RpmException('No spec file.')
852
853         def _line_for_new_section(self, line):
854             if isinstance(self.current_section, RpmCopyright):
855                 if not re_comment.match(line):
856                     return RpmPreamble
857
858             for (regexp, newclass) in self.section_starts:
859                 if regexp.match(line):
860                     return newclass
861
862             return None
863
864
865         self.current_section = RpmCopyright()
866
867         while True:
868             line = self.fin.readline()
869             if len(line) == 0:
870                 break
871             # Remove \n to make it easier to parse things
872             line = line[:-1]
873
874             new_class = _line_for_new_section(self, line)
875             if new_class:
876                 self.current_section.output(self.fout)
877                 self.current_section = new_class()
878
879             self.current_section.add(line)
880
881         self.current_section.output(self.fout)
882
883
884     def __del__(self):
885         if self.fin:
886             self.fin.close()
887             self.fin = None
888         if self.fout:
889             self.fout.close()
890             self.fout = None
891
892
893 #######################################################################
894
895
896 def main(args):
897     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.')
898
899     parser.add_option("-i", "--inline", action="store_true", dest="inline",
900                       default=False, help="edit the file inline")
901     parser.add_option("-o", "--output", dest="output",
902                       help="output file")
903     parser.add_option("-f", "--force", action="store_true", dest="force",
904                       default=False, help="overwrite output file if already existing")
905     parser.add_option("-v", "--version", action="store_true", dest="version",
906                       default=False, help="display version (" + VERSION + ")")
907
908     (options, args) = parser.parse_args()
909
910     if options.version:
911         print 'spec-cleaner ' + VERSION
912         return 0
913
914     if len(args) != 1:
915         parser.print_help()
916         return 1
917
918     spec = os.path.expanduser(args[0])
919     if options.output:
920         options.output = os.path.expanduser(options.output)
921
922     if options.output == spec:
923         options.output = ''
924         options.inline = True
925
926     if options.output and options.inline:
927         print >> sys.stderr,  'Conflicting options: --inline and --output.'
928         return 1
929
930     try:
931         cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force)
932         cleaner.run()
933     except RpmException, e:
934         print >> sys.stderr, '%s' % e
935         return 1
936
937     return 0
938
939 if __name__ == '__main__':
940     try:
941         res = main(sys.argv)
942         sys.exit(res)
943     except KeyboardInterrupt:
944         pass