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