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