use parser.print_help() if there is wrong number of arguments
[opensuse:spec-cleaner.git] / spec-cleaner
1 #!/usr/bin/env python
2 # vim: set ts=4 sw=4 et: coding=UTF-8
3
4 #
5 # Copyright (c) 2009, Novell, Inc.
6 # All rights reserved.
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are met:
10 #
11 #  * Redistributions of source code must retain the above copyright notice,
12 #    this list of conditions and the following disclaimer.
13 #  * Redistributions in binary form must reproduce the above copyright notice,
14 #    this list of conditions and the following disclaimer in the documentation
15 #    and/or other materials provided with the distribution.
16 #  * Neither the name of the <ORGANIZATION> nor the names of its contributors
17 #    may be used to endorse or promote products derived from this software
18 #    without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 # POSSIBILITY OF SUCH DAMAGE.
31 #
32 #
33 # (Licensed under the simplified BSD license)
34 #
35 # Authors:
36 #   Vincent Untz <vuntz@novell.com>
37 #   Pavol Rusnak <prusnak@opensuse.org>
38 #
39
40 import os
41 import sys
42
43 import cStringIO
44 import optparse
45 import re
46 import time
47
48 #######################################################################
49
50 VERSION = '0.1'
51
52 re_comment = re.compile('^$|^\s*#')
53 re_define = re.compile('^\s*%define', re.IGNORECASE)
54
55 re_bindir = re.compile('%{_prefix}/bin([/\s$])')
56 re_sbindir = re.compile('%{_prefix}/sbin([/\s$])')
57 re_includedir = re.compile('%{_prefix}/include([/\s$])')
58 re_datadir = re.compile('%{_prefix}/share([/\s$])')
59 re_mandir = re.compile('%{_datadir}/man([/\s$])')
60 re_infodir = re.compile('%{_datadir}/info([/\s$])')
61
62
63 def strip_useless_spaces(s):
64     return ' '.join(s.split())
65
66
67 def replace_known_dirs(s):
68     s = s.replace('%_prefix', '%{_prefix}')
69     s = s.replace('%_usr', '%{_prefix}')
70     s = s.replace('%{_usr}', '%{_prefix}')
71     s = s.replace('%_bindir', '%{_bindir}')
72     s = s.replace('%_sbindir', '%{_sbindir}')
73     s = s.replace('%_includedir', '%{_includedir}')
74     s = s.replace('%_datadir', '%{_datadir}')
75     s = s.replace('%_mandir', '%{_mandir}')
76     s = s.replace('%_infodir', '%{_infodir}')
77     s = s.replace('%_libdir', '%{_libdir}')
78     s = s.replace('%_libexecdir', '%{_libexecdir}')
79     s = s.replace('%_lib', '%{_lib}')
80     s = s.replace('%{_prefix}/%{_lib}', '%{_libdir}')
81     s = s.replace('%_sysconfdir', '%{_sysconfdir}')
82     s = s.replace('%_localstatedir', '%{_localstatedir}')
83     s = s.replace('%_var', '%{_localstatedir}')
84     s = s.replace('%{_var}', '%{_localstatedir}')
85     s = s.replace('%_initddir', '%{_initddir}')
86     # old typo in rpm macro
87     s = s.replace('%_initrddir', '%{_initddir}')
88     s = s.replace('%{_initrddir}', '%{_initddir}')
89
90     s = re_bindir.sub(r'%{_bindir}\1', s)
91     s = re_sbindir.sub(r'%{_sbindir}\1', s)
92     s = re_includedir.sub(r'%{_includedir}\1', s)
93     s = re_datadir.sub(r'%{_datadir}\1', s)
94     s = re_mandir.sub(r'%{_mandir}\1', s)
95     s = re_infodir.sub(r'%{_infodir}\1', s)
96
97     return s
98
99
100 def replace_buildroot(s):
101     s = s.replace('${RPM_BUILD_ROOT}', '%{buildroot}')
102     s = s.replace('$RPM_BUILD_ROOT', '%{buildroot}')
103     s = s.replace('%buildroot', '%{buildroot}')
104     s = s.replace('%{buildroot}/etc/init.d/', '%{buildroot}%{_initddir}/')
105     s = s.replace('%{buildroot}/etc/', '%{buildroot}%{_sysconfdir}/')
106     s = s.replace('%{buildroot}/usr/', '%{buildroot}%{_prefix}/')
107     s = s.replace('%{buildroot}/var/', '%{buildroot}%{_localstatedir}/')
108     s = s.replace('"%{buildroot}"', '%{buildroot}')
109     return s
110
111
112 def replace_optflags(s):
113     s = s.replace('${RPM_OPT_FLAGS}', '%{optflags}')
114     s = s.replace('$RPM_OPT_FLAGS', '%{optflags}')
115     s = s.replace('%optflags', '%{optflags}')
116     return s
117
118
119 def replace_remove_la(s):
120     cmp_line = strip_useless_spaces(s)
121     if cmp_line in [ 'find %{buildroot} -type f -name "*.la" -exec %{__rm} -fv {} +', 'find %{buildroot} -type f -name "*.la" -delete' ]:
122         s = 'find %{buildroot} -type f -name "*.la" -delete -print'
123     return s
124
125
126 def replace_utils(s):
127     # take care of all utilities macros that bloat spec file
128     r = {'id_u': 'id -u', 'ln_s': 'ln -s', 'lzma': 'xz --format-lzma', 'mkdir_p': 'mkdir -p', 'awk':'gawk', 'cc':'gcc', 'cpp':'gcc -E', 'cxx':'g++', 'remsh':'rsh', }
129     for i in r:
130       s = s.replace('%__'+i, r[i])
131       s = s.replace('%{__'+i+'}', r[i])
132
133     for i in [ 'aclocal', 'ar', 'as', 'autoconf', 'autoheader', 'automake', 'bzip2', 'cat', 'chgrp', 'chmod', 'chown', 'cp', 'cpio', 'file', 'gpg', 'grep', 'gzip', 'id', 'install', 'ld', 'libtoolize', 'make', 'mkdir', 'mv', 'nm', 'objcopy', 'objdump', 'patch', 'perl', 'python', 'ranlib', 'restorecon', 'rm', 'rsh', 'sed', 'semodule', 'ssh', 'strip', 'tar', 'unzip', 'xz', ]:
134         s = s.replace('%__'+i, i)
135         s = s.replace('%{__'+i+'}', i)
136
137     return s
138
139
140 def replace_buildservice(s):
141     for i in ['centos', 'debian', 'fedora', 'mandriva', 'meego', 'rhel', 'sles', 'suse', 'ubuntu']:
142         s = s.replace('%' + i + '_version','0%{?' + i + '_version}')
143         s = s.replace('%{' + i + '_version}','0%{?' + i + '_version}')
144     return s
145
146
147 def replace_all(s):
148     s = replace_buildroot(s)
149     s = replace_optflags(s)
150     s = replace_known_dirs(s)
151     s = replace_remove_la(s)
152     s = replace_utils(s)
153     s = replace_buildservice(s)
154     return s
155
156
157 #######################################################################
158
159
160 class RpmException(Exception):
161     pass
162
163
164 #######################################################################
165
166
167 class RpmSection(object):
168     '''
169         Basic cleanup: we remove trailing spaces.
170     '''
171
172     def __init__(self):
173         self.lines = []
174         self.previous_line = None
175
176     def add(self, line):
177         line = line.rstrip()
178         line = replace_all(line)
179         self.lines.append(line)
180         self.previous_line = line
181
182     def output(self, fout):
183         for line in self.lines:
184             fout.write(line + '\n')
185
186
187 #######################################################################
188
189
190 class RpmCopyright(RpmSection):
191     '''
192         Adds default copyright notice if needed.
193         Remove initial empty lines.
194         Remove norootforbuild.
195     '''
196
197
198     def _add_default_copyright(self):
199         self.lines.append(time.strftime('''#
200 # spec file for package
201 #
202 # Copyright (c) %Y SUSE LINUX Products GmbH, Nuernberg, Germany.
203 #
204 # All modifications and additions to the file contributed by third parties
205 # remain the property of their copyright owners, unless otherwise agreed
206 # upon. The license for this file, and modifications and additions to the
207 # file, is the same license as for the pristine package itself (unless the
208 # license for the pristine package is not an Open Source License, in which
209 # case the license is the MIT License). An "Open Source License" is a
210 # license that conforms to the Open Source Definition (Version 1.9)
211 # published by the Open Source Initiative.
212
213 # Please submit bugfixes or comments via http://bugs.opensuse.org/
214 #
215
216 '''))
217
218
219     def add(self, line):
220         if not self.lines and not line:
221             return
222
223         if line == '# norootforbuild':
224             return
225
226         RpmSection.add(self, line)
227
228
229     def output(self, fout):
230         if not self.lines:
231             self._add_default_copyright()
232         RpmSection.output(self, fout)
233
234
235 #######################################################################
236
237
238 class RpmPreamble(RpmSection):
239     '''
240         Only keep one empty line for many consecutive ones.
241         Reorder lines.
242         Fix bad licenses.
243         Use one line per BuildRequires/Requires/etc.
244         Use %{version} instead of %{version}-%{release} for BuildRequires/etc.
245         Remove AutoReqProv.
246         Standardize BuildRoot.
247
248         This one is a bit tricky since we reorder things. We have a notion of
249         paragraphs, categories, and groups.
250
251         A paragraph is a list of non-empty lines. Conditional directives like
252         %if/%else/%endif also mark paragraphs. It contains categories.
253         A category is a list of lines on the same topic. It contains a list of
254         groups.
255         A group is a list of lines where the first few ones are either %define
256         or comment lines, and the last one is a normal line.
257
258         This means that the %define and comments will stay attached to one
259         line, even if we reorder the lines.
260     '''
261
262     re_if = re.compile('^\s*(?:%if\s|%ifarch\s|%ifnarch\s|%else\s*$|%endif\s*$)', re.IGNORECASE)
263
264     re_name = re.compile('^Name:\s*(\S*)', re.IGNORECASE)
265     re_version = re.compile('^Version:\s*(\S*)', re.IGNORECASE)
266     re_release = re.compile('^Release:\s*(\S*)', re.IGNORECASE)
267     re_license = re.compile('^License:\s*(.*)', re.IGNORECASE)
268     re_summary = re.compile('^Summary:\s*(.*)', re.IGNORECASE)
269     re_url = re.compile('^Url:\s*(\S*)', re.IGNORECASE)
270     re_group = re.compile('^Group:\s*(.*)', re.IGNORECASE)
271     re_source = re.compile('^Source(\d*):\s*(\S*)', re.IGNORECASE)
272     re_patch = re.compile('^((?:#[#\s]*)?)Patch(\d*):\s*(\S*)', re.IGNORECASE)
273     re_buildrequires = re.compile('^BuildRequires:\s*(.*)', re.IGNORECASE)
274     re_prereq = re.compile('^PreReq:\s*(.*)', re.IGNORECASE)
275     re_requires = re.compile('^Requires:\s*(.*)', re.IGNORECASE)
276     re_recommends = re.compile('^Recommends:\s*(.*)', re.IGNORECASE)
277     re_suggests = re.compile('^Suggests:\s*(.*)', re.IGNORECASE)
278     re_supplements = re.compile('^Supplements:\s*(.*)', re.IGNORECASE)
279     re_provides = re.compile('^Provides:\s*(.*)', re.IGNORECASE)
280     re_obsoletes = re.compile('^Obsoletes:\s*(.*)', re.IGNORECASE)
281     re_buildroot = re.compile('^\s*BuildRoot:', re.IGNORECASE)
282     re_buildarch = re.compile('^\s*BuildArch:\s*(.*)', re.IGNORECASE)
283
284     re_requires_token = re.compile('(\s*(\S+(?:\s*(?:[<>]=?|=)\s*[^\s,]+)?),?)')
285
286     category_to_re = {
287         'name': re_name,
288         'version': re_version,
289         'release': re_release,
290         'license': re_license,
291         'summary': re_summary,
292         'url': re_url,
293         'group': re_group,
294         # for source, we have a special match to keep the source number
295         # for patch, we have a special match to keep the patch number
296         'buildrequires': re_buildrequires,
297         'prereq': re_prereq,
298         'requires': re_requires,
299         'recommends': re_recommends,
300         'suggests': re_suggests,
301         'supplements': re_supplements,
302         # for provides/obsoletes, we have a special case because we group them
303         # for build root, we have a special match because we force its value
304         'buildarch': re_buildarch
305     }
306
307     category_to_key = {
308         'name': 'Name',
309         'version': 'Version',
310         'release': 'Release',
311         'license': 'License',
312         'summary': 'Summary',
313         'url': 'Url',
314         'group': 'Group',
315         'source': 'Source',
316         'patch': 'Patch',
317         'buildrequires': 'BuildRequires',
318         'prereq': 'PreReq',
319         'requires': 'Requires',
320         'recommends': 'Recommends',
321         'suggests': 'Suggests',
322         'supplements': 'Supplements',
323         # Provides/Obsoletes cannot be part of this since we want to keep them
324         # mixed, so we'll have to specify the key when needed
325         'buildroot': 'BuildRoot',
326         'buildarch': 'BuildArch'
327     }
328
329     category_to_fixer = {
330     }
331
332     license_fixes = {
333         'LGPL v2.0 only': 'LGPLv2.0',
334         'LGPL v2.0 or later': 'LGPLv2.0+',
335         'LGPL v2.1 only': 'LGPLv2.1',
336         'LGPL v2.1 or later': 'LGPLv2.1+',
337         'LGPL v3 only': 'LGPLv3',
338         'LGPL v3 or later': 'LGPLv3+',
339         'GPL v2 only': 'GPLv2',
340         'GPL v2 or later': 'GPLv2+',
341         'GPL v3 only': 'GPLv3',
342         'GPL v3 or later': 'GPLv3+'
343     }
344
345     categories_order = [ 'name', 'version', 'release', 'license', 'summary', 'url', 'group', 'source', 'patch', 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements', 'provides_obsoletes', 'buildroot', 'buildarch', 'misc' ]
346
347     categories_with_sorted_package_tokens = [ 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements' ]
348     categories_with_package_tokens = categories_with_sorted_package_tokens[:]
349     categories_with_package_tokens.append('provides_obsoletes')
350
351     re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE)
352
353
354     def __init__(self):
355         RpmSection.__init__(self)
356         self._start_paragraph()
357
358
359     def _start_paragraph(self):
360         self.paragraph = {}
361         for i in self.categories_order:
362             self.paragraph[i] = []
363         self.current_group = []
364
365
366     def _add_group(self, group):
367         t = type(group)
368
369         if t == str:
370             RpmSection.add(self, group)
371         elif t == list:
372             for subgroup in group:
373                 self._add_group(subgroup)
374         else:
375             raise RpmException('Unknown type of group in preamble: %s' % t)
376
377
378     def _end_paragraph(self):
379         def sort_helper_key(a):
380             t = type(a)
381             if t == str:
382                 return a
383             elif t == list:
384                 return a[-1]
385             else:
386                 raise RpmException('Unknown type during sort: %s' % t)
387
388         for i in self.categories_order:
389             if i in self.categories_with_sorted_package_tokens:
390                 self.paragraph[i].sort(key=sort_helper_key)
391             for group in self.paragraph[i]:
392                 self._add_group(group)
393         if self.current_group:
394             # the current group was not added to any category. It's just some
395             # random stuff that should be at the end anyway.
396             self._add_group(self.current_group)
397
398         self._start_paragraph()
399
400
401     def _fix_license(self, value):
402         licenses = value.split(';')
403         for (index, license) in enumerate(licenses):
404             license = strip_useless_spaces(license)
405             if self.license_fixes.has_key(license):
406                 license = self.license_fixes[license]
407             licenses[index] = license
408
409         return [ ' ; '.join(licenses) ]
410
411     category_to_fixer['license'] = _fix_license
412
413
414     def _fix_list_of_packages(self, value):
415         if self.re_requires_token.match(value):
416             tokens = [ item[1] for item in self.re_requires_token.findall(value) ]
417             for (index, token) in enumerate(tokens):
418                 token = token.replace('%{version}-%{release}', '%{version}')
419                 tokens[index] = token
420
421             tokens.sort()
422             return tokens
423         else:
424             return [ value ]
425
426     for i in categories_with_package_tokens:
427         category_to_fixer[i] = _fix_list_of_packages
428
429
430     def _add_line_value_to(self, category, value, key = None):
431         """
432             Change a key-value line, to make sure we have the right spacing.
433
434             Note: since we don't have a key <-> category matching, we need to
435             redo one. (Eg: Provides and Obsoletes are in the same category)
436         """
437         keylen = len('BuildRequires:  ')
438
439         if key:
440             pass
441         elif self.category_to_key.has_key(category):
442             key = self.category_to_key[category]
443         else:
444             raise RpmException('Unhandled category in preamble: %s' % category)
445
446         key += ':'
447         while len(key) < keylen:
448             key += ' '
449
450         if self.category_to_fixer.has_key(category):
451             values = self.category_to_fixer[category](self, value)
452         else:
453             values = [ value ]
454
455         for value in values:
456             line = key + value
457             self._add_line_to(category, line)
458
459
460     def _add_line_to(self, category, line):
461         if self.current_group:
462             self.current_group.append(line)
463             self.paragraph[category].append(self.current_group)
464             self.current_group = []
465         else:
466             self.paragraph[category].append(line)
467
468         self.previous_line = line
469
470
471     def add(self, line):
472         if len(line) == 0:
473             if not self.previous_line or len(self.previous_line) == 0:
474                 return
475
476             # we put the empty line in the current group (so we don't list it),
477             # and write the paragraph
478             self.current_group.append(line)
479             self._end_paragraph()
480             self.previous_line = line
481             return
482
483         elif self.re_if.match(line):
484             # %if/%else/%endif marks the end of the previous paragraph
485             # We append the line at the end of the previous paragraph, though,
486             # since it will stay at the end there. If putting it at the
487             # beginning of the next paragraph, it will likely move (with the
488             # misc category).
489             self.current_group.append(line)
490             self._end_paragraph()
491             self.previous_line = line
492             return
493
494         elif re_comment.match(line) or re_define.match(line):
495             self.current_group.append(line)
496             self.previous_line = line
497             return
498
499         elif self.re_autoreqprov.match(line):
500             return
501
502         elif self.re_source.match(line):
503             match = self.re_source.match(line)
504             self._add_line_value_to('source', match.group(2), key = 'Source%s' % match.group(1))
505             return
506
507         elif self.re_patch.match(line):
508             # FIXME: this is not perfect, but it's good enough for most cases
509             if not self.previous_line or not re_comment.match(self.previous_line):
510                 self.current_group.append('# PATCH-MISSING-TAG -- See http://en.opensuse.org/Packaging/Patches')
511
512             match = self.re_patch.match(line)
513             self._add_line_value_to('source', match.group(3), key = '%sPatch%s' % (match.group(1), match.group(2)))
514             return
515
516         elif self.re_provides.match(line):
517             match = self.re_provides.match(line)
518             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Provides')
519             return
520
521         elif self.re_obsoletes.match(line):
522             match = self.re_obsoletes.match(line)
523             self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Obsoletes')
524             return
525
526         elif self.re_buildroot.match(line):
527             if len(self.paragraph['buildroot']) == 0:
528                 self._add_line_value_to('buildroot', '%{_tmppath}/%{name}-%{version}-build')
529             return
530
531         else:
532             for (category, regexp) in self.category_to_re.iteritems():
533                 match = regexp.match(line)
534                 if match:
535                     self._add_line_value_to(category, match.group(1))
536                     return
537
538             self._add_line_to('misc', line)
539
540
541     def output(self, fout):
542         self._end_paragraph()
543         RpmSection.output(self, fout)
544
545
546 #######################################################################
547
548
549 class RpmPackage(RpmPreamble):
550     '''
551         We handle this the same was as the preamble.
552     '''
553
554     def add(self, line):
555         # The first line (%package) should always be added and is different
556         # from the lines we handle in RpmPreamble.
557         if self.previous_line is None:
558             RpmSection.add(self, line)
559             return
560
561         RpmPreamble.add(self, line)
562
563
564 #######################################################################
565
566
567 class RpmDescription(RpmSection):
568     '''
569         Only keep one empty line for many consecutive ones.
570         Remove Authors from description.
571     '''
572
573     def __init__(self):
574         RpmSection.__init__(self)
575         self.removing_authors = False
576         # Tracks the use of a macro. When this happens and we're still in a
577         # description, we actually don't know where we are so we just put all
578         # the following lines blindly, without trying to fix anything.
579         self.unknown_line = False
580
581     def add(self, line):
582         lstrip = line.lstrip()
583         if self.previous_line != None and len(lstrip) > 0 and lstrip[0] == '%':
584             self.unknown_line = True
585
586         if self.removing_authors and not self.unknown_line:
587             return
588
589         if len(line) == 0:
590             if not self.previous_line or len(self.previous_line) == 0:
591                 return
592
593         if line == 'Authors:':
594             self.removing_authors = True
595             return
596
597         RpmSection.add(self, line)
598
599
600 #######################################################################
601
602
603 class RpmPrep(RpmSection):
604     '''
605         Try to simplify to %setup -q when possible.
606     '''
607
608     def add(self, line):
609         if line.startswith('%setup'):
610             cmp_line = line.replace(' -q', '')
611             cmp_line = cmp_line.replace(' -n %{name}-%{version}', '')
612             cmp_line = strip_useless_spaces(cmp_line)
613             if cmp_line == '%setup':
614                 line = '%setup -q'
615
616         RpmSection.add(self, line)
617
618
619 #######################################################################
620
621
622 class RpmBuild(RpmSection):
623     '''
624         Replace %{?jobs:-j%jobs} (suse-ism) with %{?_smp_mflags}
625     '''
626
627     def add(self, line):
628         if not re_comment.match(line):
629             line = line.replace('%{?jobs:-j%jobs}' , '%{?_smp_mflags}')
630             line = line.replace('%{?jobs: -j%jobs}', '%{?_smp_mflags}')
631
632         RpmSection.add(self, line)
633
634
635 #######################################################################
636
637
638 class RpmInstall(RpmSection):
639     '''
640         Remove commands that wipe out the build root.
641         Use %make_install macro.
642         Replace %makeinstall (suse-ism).
643     '''
644
645     def add(self, line):
646         # remove double spaces when comparing the line
647         cmp_line = strip_useless_spaces(line)
648         cmp_line = replace_buildroot(cmp_line)
649
650         if cmp_line.find('DESTDIR=%{buildroot}') != -1:
651             buf = cmp_line.replace('DESTDIR=%{buildroot}', '')
652             buf = strip_useless_spaces(buf)
653             if buf == 'make install':
654                 line = '%make_install'
655         elif cmp_line == '%makeinstall':
656             line = '%make_install'
657         elif cmp_line == 'rm -rf %{buildroot}':
658             return
659
660         RpmSection.add(self, line)
661
662
663 #######################################################################
664
665
666 class RpmClean(RpmSection):
667     # if the section contains just rm -rf %{buildroot} then remove the whole section (including %clean)
668     pass
669
670
671 #######################################################################
672
673
674 class RpmScriptlets(RpmSection):
675     '''
676         Do %post -p /sbin/ldconfig when possible.
677     '''
678
679     def __init__(self):
680         RpmSection.__init__(self)
681         self.cache = []
682
683
684     def add(self, line):
685         if len(self.lines) == 0:
686             if not self.cache:
687                 if line.find(' -p ') == -1 and line.find(' -f ') == -1:
688                     self.cache.append(line)
689                     return
690             else:
691                 if line in ['', '/sbin/ldconfig' ]:
692                     self.cache.append(line)
693                     return
694                 else:
695                     for cached in self.cache:
696                         RpmSection.add(self, cached)
697                     self.cache = None
698
699         RpmSection.add(self, line)
700
701
702     def output(self, fout):
703         if self.cache:
704             RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig')
705             RpmSection.add(self, '')
706
707         RpmSection.output(self, fout)
708
709
710 #######################################################################
711
712
713 class RpmFiles(RpmSection):
714     """
715         Replace additional /usr, /etc and /var because we're sure we can use
716         macros there.
717
718         Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with
719         '%{_includedir}/mux/'
720     """
721
722     re_etcdir = re.compile('(^|\s)/etc/')
723     re_usrdir = re.compile('(^|\s)/usr/')
724     re_vardir = re.compile('(^|\s)/var/')
725
726     re_dir = re.compile('^\s*%dir\s*(\S+)\s*')
727
728     def __init__(self):
729         RpmSection.__init__(self)
730         self.dir_on_previous_line = None
731
732
733     def add(self, line):
734         line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line)
735         line = self.re_usrdir.sub(r'\1%{_prefix}/', line)
736         line = self.re_vardir.sub(r'\1%{_localstatedir}/', line)
737
738         if self.dir_on_previous_line:
739             if line == self.dir_on_previous_line + '/*':
740                 RpmSection.add(self, self.dir_on_previous_line + '/')
741                 self.dir_on_previous_line = None
742                 return
743             else:
744                 RpmSection.add(self, '%dir ' + self.dir_on_previous_line)
745                 self.dir_on_previous_line = None
746
747         match = self.re_dir.match(line)
748         if match:
749             self.dir_on_previous_line = match.group(1)
750             return
751
752         RpmSection.add(self, line)
753
754
755 #######################################################################
756
757
758 class RpmChangelog(RpmSection):
759     '''
760         Remove changelog entries.
761     '''
762
763     def add(self, line):
764         # only add the first line (%changelog)
765         if len(self.lines) == 0:
766             RpmSection.add(self, line)
767
768
769 #######################################################################
770
771
772 class RpmSpecCleaner:
773
774     specfile = None
775     fin = None
776     fout = None
777     current_section = None
778
779
780     re_spec_package = re.compile('^%package\s*', re.IGNORECASE)
781     re_spec_description = re.compile('^%description\s*', re.IGNORECASE)
782     re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE)
783     re_spec_build = re.compile('^%build\s*$', re.IGNORECASE)
784     re_spec_install = re.compile('^%install\s*$', re.IGNORECASE)
785     re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE)
786     re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE)
787     re_spec_files = re.compile('^%files\s*', re.IGNORECASE)
788     re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE)
789
790
791     section_starts = [
792         (re_spec_package, RpmPackage),
793         (re_spec_description, RpmDescription),
794         (re_spec_prep, RpmPrep),
795         (re_spec_build, RpmBuild),
796         (re_spec_install, RpmInstall),
797         (re_spec_clean, RpmClean),
798         (re_spec_scriptlets, RpmScriptlets),
799         (re_spec_files, RpmFiles),
800         (re_spec_changelog, RpmChangelog)
801     ]
802
803
804     def __init__(self, specfile, output, inline, force):
805         if not specfile.endswith('.spec'):
806             raise RpmException('%s does not appear to be a spec file.' % specfile)
807
808         if not os.path.exists(specfile):
809             raise RpmException('%s does not exist.' % specfile)
810
811         self.specfile = specfile
812         self.output = output
813         self.inline = inline
814
815         self.fin = open(self.specfile)
816
817         if self.output:
818             if not force and os.path.exists(self.output):
819                 raise RpmException('%s already exists.' % self.output)
820             self.fout = open(self.output, 'w')
821         elif self.inline:
822             io = cStringIO.StringIO()
823             while True:
824                 bytes = self.fin.read(500 * 1024)
825                 if len(bytes) == 0:
826                     break
827                 io.write(bytes)
828
829             self.fin.close()
830             io.seek(0)
831             self.fin = io
832             self.fout = open(self.specfile, 'w')
833         else:
834             self.fout = sys.stdout
835
836
837     def run(self):
838         if not self.specfile or not self.fin:
839             raise RpmException('No spec file.')
840
841         def _line_for_new_section(self, line):
842             if isinstance(self.current_section, RpmCopyright):
843                 if not re_comment.match(line):
844                     return RpmPreamble
845
846             for (regexp, newclass) in self.section_starts:
847                 if regexp.match(line):
848                     return newclass
849
850             return None
851
852
853         self.current_section = RpmCopyright()
854
855         while True:
856             line = self.fin.readline()
857             if len(line) == 0:
858                 break
859             # Remove \n to make it easier to parse things
860             line = line[:-1]
861
862             new_class = _line_for_new_section(self, line)
863             if new_class:
864                 self.current_section.output(self.fout)
865                 self.current_section = new_class()
866
867             self.current_section.add(line)
868
869         self.current_section.output(self.fout)
870
871
872     def __del__(self):
873         if self.fin:
874             self.fin.close()
875             self.fin = None
876         if self.fout:
877             self.fout.close()
878             self.fout = None
879
880
881 #######################################################################
882
883
884 def main(args):
885     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.')
886
887     parser.add_option("-i", "--inline", action="store_true", dest="inline",
888                       default=False, help="edit the file inline")
889     parser.add_option("-o", "--output", dest="output",
890                       help="output file")
891     parser.add_option("-f", "--force", action="store_true", dest="force",
892                       default=False, help="overwrite output file if already existing")
893     parser.add_option("-v", "--version", action="store_true", dest="version",
894                       default=False, help="display version (" + VERSION + ")")
895
896     (options, args) = parser.parse_args()
897
898     if options.version:
899         print 'spec-cleaner ' + VERSION
900         return 0
901
902     if len(args) != 1:
903         parser.print_help()
904         return 1
905
906     spec = os.path.expanduser(args[0])
907     if options.output:
908         options.output = os.path.expanduser(options.output)
909
910     if options.output == spec:
911         options.output = ''
912         options.inline = True
913
914     if options.output and options.inline:
915         print >> sys.stderr,  'Conflicting options: --inline and --output.'
916         return 1
917
918     try:
919         cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force)
920         cleaner.run()
921     except RpmException, e:
922         print >> sys.stderr, '%s' % e
923         return 1
924
925     return 0
926
927 if __name__ == '__main__':
928     try:
929         res = main(sys.argv)
930         sys.exit(res)
931     except KeyboardInterrupt:
932         pass