2 # vim: set ts=4 sw=4 et: coding=UTF-8
5 # Copyright (c) 2009, Novell, Inc.
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are met:
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.
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.
33 # (Licensed under the simplified BSD license)
36 # Vincent Untz <vuntz@novell.com>
37 # Pavol Rusnak <prusnak@opensuse.org>
38 # Petr Uzel <petr.uzel@suse.cz>
52 #######################################################################
56 re_comment = re.compile('^$|^\s*#')
57 re_define = re.compile('^\s*%define', re.IGNORECASE)
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$])')
67 def strip_useless_spaces(s):
68 return ' '.join(s.split())
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}')
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)
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}')
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}')
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'
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', }
134 s = s.replace('%__' + i, r[i])
135 s = s.replace('%{__' + i + '}', r[i])
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)
144 def replace_buildservice(s):
145 for i in ['centos', 'debian', 'fedora', 'mandriva', 'meego', 'rhel', 'sles', 'suse', 'ubuntu']:
146 s = s.replace('%' + i + '_version', '0%{?' + i + '_version}')
147 s = s.replace('%{' + i + '_version}', '0%{?' + i + '_version}')
150 def replace_macros(s):
151 for i in ['name', 'version', 'release']:
152 s = s.replace('%' + i, '%{' + i + '}')
156 s = replace_buildroot(s)
157 s = replace_optflags(s)
158 s = replace_known_dirs(s)
159 s = replace_remove_la(s)
161 s = replace_buildservice(s)
162 s = replace_macros(s)
166 #######################################################################
169 class RpmException(Exception):
173 #######################################################################
176 class RpmSection(object):
178 Basic cleanup: we remove trailing spaces.
183 self.previous_line = None
187 line = replace_all(line)
188 self.lines.append(line)
189 self.previous_line = line
191 def output(self, fout):
192 for line in self.lines:
193 fout.write(line + '\n')
196 #######################################################################
199 class RpmCopyright(RpmSection):
201 Adds default copyright notice if needed.
202 Remove initial empty lines.
203 Remove norootforbuild.
207 def _add_default_copyright(self):
208 self.lines.append(time.strftime('''#
209 # Please submit bugfixes or comments via http://bugs.opensuse.org/
215 if not self.lines and not line:
218 if line.startswith('# norootforbuild') or \
219 line.startswith('# usedforbuild'):
222 RpmSection.add(self, line)
225 def output(self, fout):
227 self._add_default_copyright()
228 RpmSection.output(self, fout)
231 #######################################################################
234 class RpmPreamble(RpmSection):
236 Only keep one empty line for many consecutive ones.
239 Use one line per BuildRequires/Requires/etc.
240 Use %{version} instead of %{version}-%{release} for BuildRequires/etc.
242 Standardize BuildRoot.
244 This one is a bit tricky since we reorder things. We have a notion of
245 paragraphs, categories, and groups.
247 A paragraph is a list of non-empty lines. Conditional directives like
248 %if/%else/%endif also mark paragraphs. It contains categories.
249 A category is a list of lines on the same topic. It contains a list of
251 A group is a list of lines where the first few ones are either %define
252 or comment lines, and the last one is a normal line.
254 This means that the %define and comments will stay attached to one
255 line, even if we reorder the lines.
258 re_if = re.compile('^\s*(?:%if\s|%ifarch\s|%ifnarch\s|%else\s*$|%endif\s*$)', re.IGNORECASE)
260 re_name = re.compile('^Name:\s*(\S*)', re.IGNORECASE)
261 re_version = re.compile('^Version:\s*(\S*)', re.IGNORECASE)
262 re_release = re.compile('^Release:\s*(\S*)', re.IGNORECASE)
263 re_license = re.compile('^License:\s*(.*)', re.IGNORECASE)
264 re_summary = re.compile('^Summary:\s*(.*)', re.IGNORECASE)
265 re_url = re.compile('^Url:\s*(\S*)', re.IGNORECASE)
266 re_group = re.compile('^Group:\s*(.*)', re.IGNORECASE)
267 re_source = re.compile('^Source(\d*):\s*(\S*)', re.IGNORECASE)
268 re_patch = re.compile('^((?:#[#\s]*)?)Patch(\d*):\s*(\S*)', re.IGNORECASE)
269 re_buildrequires = re.compile('^BuildRequires:\s*(.*)', re.IGNORECASE)
270 re_prereq = re.compile('^PreReq:\s*(.*)', re.IGNORECASE)
271 re_requires = re.compile('^Requires:\s*(.*)', re.IGNORECASE)
272 re_recommends = re.compile('^Recommends:\s*(.*)', re.IGNORECASE)
273 re_suggests = re.compile('^Suggests:\s*(.*)', re.IGNORECASE)
274 re_supplements = re.compile('^Supplements:\s*(.*)', re.IGNORECASE)
275 re_provides = re.compile('^Provides:\s*(.*)', re.IGNORECASE)
276 re_obsoletes = re.compile('^Obsoletes:\s*(.*)', re.IGNORECASE)
277 re_buildroot = re.compile('^\s*BuildRoot:', re.IGNORECASE)
278 re_buildarch = re.compile('^\s*BuildArch:\s*(.*)', re.IGNORECASE)
280 re_requires_token = re.compile('(\s*(\S+(?:\s*(?:[<>]=?|=)\s*[^\s,]+)?),?)')
284 'version': re_version,
285 'release': re_release,
286 'license': re_license,
287 'summary': re_summary,
290 # for source, we have a special match to keep the source number
291 # for patch, we have a special match to keep the patch number
292 'buildrequires': re_buildrequires,
294 'requires': re_requires,
295 'recommends': re_recommends,
296 'suggests': re_suggests,
297 'supplements': re_supplements,
298 # for provides/obsoletes, we have a special case because we group them
299 # for build root, we have a special match because we force its value
300 'buildarch': re_buildarch
305 'version': 'Version',
306 'release': 'Release',
307 'license': 'License',
308 'summary': 'Summary',
313 'buildrequires': 'BuildRequires',
315 'requires': 'Requires',
316 'recommends': 'Recommends',
317 'suggests': 'Suggests',
318 'supplements': 'Supplements',
319 # Provides/Obsoletes cannot be part of this since we want to keep them
320 # mixed, so we'll have to specify the key when needed
321 'buildroot': 'BuildRoot',
322 'buildarch': 'BuildArch'
325 category_to_fixer = {
329 'LGPL v2.0 only': 'LGPLv2.0',
330 'LGPL v2.0 or later': 'LGPLv2.0+',
331 'LGPL v2.1 only': 'LGPLv2.1',
332 'LGPL v2.1 or later': 'LGPLv2.1+',
333 'LGPL v3 only': 'LGPLv3',
334 'LGPL v3 or later': 'LGPLv3+',
335 'GPL v2 only': 'GPLv2',
336 'GPL v2 or later': 'GPLv2+',
337 'GPL v3 only': 'GPLv3',
338 'GPL v3 or later': 'GPLv3+'
341 categories_order = [ 'name', 'version', 'release', 'license', 'summary', 'url', 'group', 'source', 'patch', 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements', 'provides_obsoletes', 'buildroot', 'buildarch', 'misc' ]
343 categories_with_sorted_package_tokens = [ 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements' ]
344 categories_with_package_tokens = categories_with_sorted_package_tokens[:]
345 categories_with_package_tokens.append('provides_obsoletes')
347 re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE)
351 RpmSection.__init__(self)
352 self._start_paragraph()
355 def _start_paragraph(self):
357 for i in self.categories_order:
358 self.paragraph[i] = []
359 self.current_group = []
362 def _add_group(self, group):
366 RpmSection.add(self, group)
368 for subgroup in group:
369 self._add_group(subgroup)
371 raise RpmException('Unknown type of group in preamble: %s' % t)
374 def _end_paragraph(self):
375 def sort_helper_key(a):
382 raise RpmException('Unknown type during sort: %s' % t)
384 for i in self.categories_order:
385 if i in self.categories_with_sorted_package_tokens:
386 self.paragraph[i].sort(key=sort_helper_key)
387 for group in self.paragraph[i]:
388 self._add_group(group)
389 if self.current_group:
390 # the current group was not added to any category. It's just some
391 # random stuff that should be at the end anyway.
392 self._add_group(self.current_group)
394 self._start_paragraph()
397 def _fix_license(self, value):
398 licenses = value.split(';')
399 for (index, license) in enumerate(licenses):
400 license = strip_useless_spaces(license)
401 if self.license_fixes.has_key(license):
402 license = self.license_fixes[license]
403 licenses[index] = license
405 return [ ' ; '.join(licenses) ]
407 category_to_fixer['license'] = _fix_license
410 def _pkgname_to_pkgconfig(self, value):
412 'cairo-devel': 'cairo',
413 'dbus-1-devel': 'dbus-1',
414 'dbus-1-glib-devel': 'dbus-glib-1',
415 'gconf2-devel': 'gconf-2.0',
416 'exo-devel': 'exo-1',
417 'glib2-devel': 'glib-2.0',
418 'gtk2-devel': 'gtk+-2.0',
420 'libexif-devel': 'libexif',
421 'libgarcon-devel': 'garcon-1',
422 'libglade2-devel': 'libglade-2.0',
423 'libgladeui-1_0-devel': 'gladeui-1.0',
424 'libgudev-1_0-devel': 'gudev-1.0',
425 'libnotify-devel': 'libnotify',
426 'libwnck-devel': 'libwnck-1.0',
427 'libxfce4ui-devel': 'libxfce4ui-1',
428 'libxfce4util-devel': 'libxfce4util-1.0',
429 'libxfcegui4-devel': 'libxfcegui4-1.0',
430 'libxfconf-devel': 'libxfconf-0',
431 'libxklavier-devel': 'libxklavier',
432 'libxml2-devel': 'libxml-2.0',
433 'startup-notification-devel': 'libstartup-notification-1.0',
434 'xfce4-panel-devel': 'libxfce4panel-1.0',
437 value = value.replace(i, 'pkgconfig('+r[i]+')')
440 def _fix_list_of_packages(self, value):
441 if self.re_requires_token.match(value):
442 tokens = [ item[1] for item in self.re_requires_token.findall(value) ]
443 for (index, token) in enumerate(tokens):
444 token = token.replace('%{version}-%{release}', '%{version}')
445 token = token.replace(' ','')
446 token = re.sub(r'([<>]=?|=)', r' \1 ', token)
447 token = self._pkgname_to_pkgconfig(token)
448 tokens[index] = token
455 for i in categories_with_package_tokens:
456 category_to_fixer[i] = _fix_list_of_packages
459 def _add_line_value_to(self, category, value, key = None):
461 Change a key-value line, to make sure we have the right spacing.
463 Note: since we don't have a key <-> category matching, we need to
464 redo one. (Eg: Provides and Obsoletes are in the same category)
466 keylen = len('BuildRequires: ')
470 elif self.category_to_key.has_key(category):
471 key = self.category_to_key[category]
473 raise RpmException('Unhandled category in preamble: %s' % category)
476 while len(key) < keylen:
479 if self.category_to_fixer.has_key(category):
480 values = self.category_to_fixer[category](self, value)
486 self._add_line_to(category, line)
489 def _add_line_to(self, category, line):
490 if self.current_group:
491 self.current_group.append(line)
492 self.paragraph[category].append(self.current_group)
493 self.current_group = []
495 self.paragraph[category].append(line)
497 self.previous_line = line
502 if not self.previous_line or len(self.previous_line) == 0:
505 # we put the empty line in the current group (so we don't list it),
506 # and write the paragraph
507 self.current_group.append(line)
508 self._end_paragraph()
509 self.previous_line = line
512 elif self.re_if.match(line):
513 # %if/%else/%endif marks the end of the previous paragraph
514 # We append the line at the end of the previous paragraph, though,
515 # since it will stay at the end there. If putting it at the
516 # beginning of the next paragraph, it will likely move (with the
518 self.current_group.append(line)
519 self._end_paragraph()
520 self.previous_line = line
523 elif re_comment.match(line) or re_define.match(line):
524 self.current_group.append(line)
525 self.previous_line = line
528 elif self.re_autoreqprov.match(line):
531 elif self.re_source.match(line):
532 match = self.re_source.match(line)
533 self._add_line_value_to('source', match.group(2), key = 'Source%s' % match.group(1))
536 elif self.re_patch.match(line):
537 # FIXME: this is not perfect, but it's good enough for most cases
538 if not self.previous_line or not re_comment.match(self.previous_line):
539 self.current_group.append('# PATCH-MISSING-TAG -- See http://en.opensuse.org/Packaging/Patches')
541 match = self.re_patch.match(line)
542 # convert Patch: to Patch0:
543 if match.group(2) == '':
547 self._add_line_value_to('source', match.group(3), key = '%sPatch%s%s' % (match.group(1), zero, match.group(2)))
550 elif self.re_provides.match(line):
551 match = self.re_provides.match(line)
552 self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Provides')
555 elif self.re_obsoletes.match(line):
556 match = self.re_obsoletes.match(line)
557 self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Obsoletes')
560 elif self.re_buildroot.match(line):
561 if len(self.paragraph['buildroot']) == 0:
562 self._add_line_value_to('buildroot', '%{_tmppath}/%{name}-%{version}-build')
566 for (category, regexp) in self.category_to_re.iteritems():
567 match = regexp.match(line)
569 self._add_line_value_to(category, match.group(1))
572 self._add_line_to('misc', line)
575 def output(self, fout):
576 self._end_paragraph()
577 RpmSection.output(self, fout)
580 #######################################################################
583 class RpmPackage(RpmPreamble):
585 We handle this the same was as the preamble.
589 # The first line (%package) should always be added and is different
590 # from the lines we handle in RpmPreamble.
591 if self.previous_line is None:
592 RpmSection.add(self, line)
595 RpmPreamble.add(self, line)
598 #######################################################################
601 class RpmDescription(RpmSection):
603 Only keep one empty line for many consecutive ones.
604 Remove Authors from description.
608 RpmSection.__init__(self)
609 self.removing_authors = False
610 # Tracks the use of a macro. When this happens and we're still in a
611 # description, we actually don't know where we are so we just put all
612 # the following lines blindly, without trying to fix anything.
613 self.unknown_line = False
616 lstrip = line.lstrip()
617 if self.previous_line != None and len(lstrip) > 0 and lstrip[0] == '%':
618 self.unknown_line = True
620 if self.removing_authors and not self.unknown_line:
624 if not self.previous_line or len(self.previous_line) == 0:
627 if line == 'Authors:':
628 self.removing_authors = True
631 RpmSection.add(self, line)
634 #######################################################################
637 class RpmPrep(RpmSection):
639 Try to simplify to %setup -q when possible.
640 Replace %patch with %patch0
644 if line.startswith('%setup'):
645 cmp_line = line.replace(' -q', '')
646 cmp_line = cmp_line.replace(' -n %{name}-%{version}', '')
647 cmp_line = strip_useless_spaces(cmp_line)
648 if cmp_line == '%setup':
650 if line.startswith('%patch ') or line == '%patch':
651 line = line.replace('%patch','%patch0')
653 RpmSection.add(self, line)
656 #######################################################################
659 class RpmBuild(RpmSection):
661 Replace %{?jobs:-j%jobs} (suse-ism) with %{?_smp_mflags}
665 if not re_comment.match(line):
666 line = line.replace('%_smp_mflags' , '%{?_smp_mflags}')
667 line = line.replace('%{_smp_mflags}' , '%{?_smp_mflags}')
668 line = line.replace('%{?jobs:-j%jobs}' , '%{?_smp_mflags}')
669 line = line.replace('%{?jobs: -j%jobs}', '%{?_smp_mflags}')
670 line = line.replace('%{?jobs:-j %jobs}', '%{?_smp_mflags}')
672 RpmSection.add(self, line)
675 #######################################################################
678 class RpmInstall(RpmSection):
680 Remove commands that wipe out the build root.
681 Use %make_install macro.
682 Replace %makeinstall (suse-ism).
686 # remove double spaces when comparing the line
687 cmp_line = strip_useless_spaces(line)
688 cmp_line = replace_buildroot(cmp_line)
690 if cmp_line.find('DESTDIR=%{buildroot}') != -1:
691 buf = cmp_line.replace('DESTDIR=%{buildroot}', '')
692 buf = strip_useless_spaces(buf)
693 if buf == 'make install' or buf == 'make install':
694 line = '%make_install'
695 elif cmp_line == '%makeinstall':
696 line = '%make_install'
697 elif cmp_line == 'rm -rf %{buildroot}':
700 RpmSection.add(self, line)
703 #######################################################################
706 class RpmClean(RpmSection):
707 # if the section contains just rm -rf %{buildroot} then remove the whole section (including %clean)
711 #######################################################################
714 class RpmScriptlets(RpmSection):
716 Do %post -p /sbin/ldconfig when possible.
720 RpmSection.__init__(self)
725 if len(self.lines) == 0:
727 if line.find(' -p ') == -1 and line.find(' -f ') == -1:
728 self.cache.append(line)
731 if line in ['', '/sbin/ldconfig' ]:
732 self.cache.append(line)
735 for cached in self.cache:
736 RpmSection.add(self, cached)
739 RpmSection.add(self, line)
742 def output(self, fout):
744 RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig')
745 RpmSection.add(self, '')
747 RpmSection.output(self, fout)
750 #######################################################################
753 class RpmFiles(RpmSection):
755 Replace additional /usr, /etc and /var because we're sure we can use
758 Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with
759 '%{_includedir}/mux/'
762 re_etcdir = re.compile('(^|\s)/etc/')
763 re_usrdir = re.compile('(^|\s)/usr/')
764 re_vardir = re.compile('(^|\s)/var/')
766 re_dir = re.compile('^\s*%dir\s*(\S+)\s*')
769 RpmSection.__init__(self)
770 self.dir_on_previous_line = None
774 line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line)
775 line = self.re_usrdir.sub(r'\1%{_prefix}/', line)
776 line = self.re_vardir.sub(r'\1%{_localstatedir}/', line)
778 if self.dir_on_previous_line:
779 if line == self.dir_on_previous_line + '/*':
780 RpmSection.add(self, self.dir_on_previous_line + '/')
781 self.dir_on_previous_line = None
784 RpmSection.add(self, '%dir ' + self.dir_on_previous_line)
785 self.dir_on_previous_line = None
787 match = self.re_dir.match(line)
789 self.dir_on_previous_line = match.group(1)
792 RpmSection.add(self, line)
795 #######################################################################
798 class RpmChangelog(RpmSection):
800 Remove changelog entries.
804 # only add the first line (%changelog)
805 if len(self.lines) == 0:
806 RpmSection.add(self, line)
809 #######################################################################
812 class RpmSpecCleaner:
817 current_section = None
819 re_spec_package = re.compile('^%package\s*', re.IGNORECASE)
820 re_spec_description = re.compile('^%description\s*', re.IGNORECASE)
821 re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE)
822 re_spec_build = re.compile('^%build\s*$', re.IGNORECASE)
823 re_spec_install = re.compile('^%install\s*$', re.IGNORECASE)
824 re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE)
825 re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE)
826 re_spec_files = re.compile('^%files\s*', re.IGNORECASE)
827 re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE)
831 (re_spec_package, RpmPackage),
832 (re_spec_description, RpmDescription),
833 (re_spec_prep, RpmPrep),
834 (re_spec_build, RpmBuild),
835 (re_spec_install, RpmInstall),
836 (re_spec_clean, RpmClean),
837 (re_spec_scriptlets, RpmScriptlets),
838 (re_spec_files, RpmFiles),
839 (re_spec_changelog, RpmChangelog)
843 def __init__(self, specfile, output, inline, force, diff, diff_prog):
844 if not specfile.endswith('.spec'):
845 raise RpmException('%s does not appear to be a spec file.' % specfile)
847 if not os.path.exists(specfile):
848 raise RpmException('%s does not exist.' % specfile)
850 self.specfile = specfile
854 self.diff_prog = diff_prog
856 self.fin = open(self.specfile)
859 if not force and os.path.exists(self.output):
860 raise RpmException('%s already exists.' % self.output)
861 self.fout = open(self.output, 'w')
863 io = cStringIO.StringIO()
865 bytes = self.fin.read(500 * 1024)
873 self.fout = open(self.specfile, 'w')
875 self.fout = tempfile.NamedTemporaryFile(prefix=self.specfile)
877 self.fout = sys.stdout
881 if not self.specfile or not self.fin:
882 raise RpmException('No spec file.')
884 def _line_for_new_section(self, line):
885 if isinstance(self.current_section, RpmCopyright):
886 if not re_comment.match(line):
889 for (regexp, newclass) in self.section_starts:
890 if regexp.match(line):
896 self.current_section = RpmCopyright()
899 line = self.fin.readline()
902 # Remove \n to make it easier to parse things
905 new_class = _line_for_new_section(self, line)
907 self.current_section.output(self.fout)
908 self.current_section = new_class()
910 self.current_section.add(line)
912 self.current_section.output(self.fout)
916 cmd = shlex.split(self.diff_prog + " " + self.specfile.replace(" ","\\ ") + " " + self.fout.name.replace(" ","\\ "))
918 subprocess.call(cmd, shell=False)
920 raise RpmException('Could not execute %s (%s)' % (self.diff_prog.split()[0], e.strerror))
931 #######################################################################
935 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.')
937 parser.add_option("-i", "--inline", action="store_true", dest="inline",
938 default=False, help="edit the file inline")
939 parser.add_option("-o", "--output", dest="output",
941 parser.add_option("-f", "--force", action="store_true", dest="force",
942 default=False, help="overwrite output file if already existing")
943 parser.add_option("-d", "--diff", action="store_true", dest="diff",
944 default=False, help="call external program to compare new and original specfile")
945 parser.add_option("--diff-prog", dest="diff_prog",
946 help="program to generate diff (implies --diff)")
947 parser.add_option("-v", "--version", action="store_true", dest="version",
948 default=False, help="display version (" + VERSION + ")")
950 (options, args) = parser.parse_args()
953 print 'spec-cleaner ' + VERSION
960 spec = os.path.expanduser(args[0])
962 options.output = os.path.expanduser(options.output)
964 if options.output == spec:
966 options.inline = True
968 if options.diff_prog:
969 # --diff-prog implies -d
972 # if diff-prog is not specified, set default here
973 options.diff_prog = "vimdiff"
975 if options.output and options.inline:
976 print >> sys.stderr, 'Conflicting options: --inline and --output.'
979 if options.diff and options.output:
980 print >> sys.stderr, 'Conflicting options: --diff and --output.'
983 if options.diff and options.inline:
984 print >> sys.stderr, 'Conflicting options: --diff and --inline.'
988 cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force, options.diff, options.diff_prog)
990 except RpmException, e:
991 print >> sys.stderr, '%s' % e
996 if __name__ == '__main__':
1000 except KeyboardInterrupt: