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