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