From 1403f7b963d71b0367961bad57d04ac7aad1e1625d08d07b534588438cc38cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Schr=C3=B6ter?= Date: Tue, 12 Oct 2010 13:33:30 +0000 Subject: [PATCH] Accepting request 49565 from home:vuntz OBS-URL: https://build.opensuse.org/request/show/49565 OBS-URL: https://build.opensuse.org/package/show/openSUSE:Tools/spec-cleaner?expand=0&rev=1 --- .gitattributes | 23 ++ .gitignore | 1 + spec-cleaner | 908 +++++++++++++++++++++++++++++++++++++++++++ spec-cleaner.changes | 5 + spec-cleaner.spec | 49 +++ 5 files changed, 986 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 spec-cleaner create mode 100644 spec-cleaner.changes create mode 100644 spec-cleaner.spec diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9b03811 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +## Default LFS +*.7z filter=lfs diff=lfs merge=lfs -text +*.bsp filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.gem filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.jar filter=lfs diff=lfs merge=lfs -text +*.lz filter=lfs diff=lfs merge=lfs -text +*.lzma filter=lfs diff=lfs merge=lfs -text +*.obscpio filter=lfs diff=lfs merge=lfs -text +*.oxt filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.rpm filter=lfs diff=lfs merge=lfs -text +*.tbz filter=lfs diff=lfs merge=lfs -text +*.tbz2 filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +*.txz filter=lfs diff=lfs merge=lfs -text +*.whl filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57affb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.osc diff --git a/spec-cleaner b/spec-cleaner new file mode 100644 index 0000000..e8392e3 --- /dev/null +++ b/spec-cleaner @@ -0,0 +1,908 @@ +#!/usr/bin/env python +# vim: set ts=4 sw=4 et: coding=UTF-8 + +# +# Copyright (c) 2009, Novell, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# +# (Licensed under the simplified BSD license) +# +# Authors: +# Vincent Untz +# Pavol Rusnak +# + +import os +import sys + +import cStringIO +import optparse +import re +import time + +####################################################################### + +VERSION = '0.1' + +re_comment = re.compile('^$|^\s*#') +re_define = re.compile('^\s*%define', re.IGNORECASE) + +re_bindir = re.compile('%{_prefix}/bin([/\s$])') +re_sbindir = re.compile('%{_prefix}/sbin([/\s$])') +re_includedir = re.compile('%{_prefix}/include([/\s$])') +re_datadir = re.compile('%{_prefix}/share([/\s$])') +re_mandir = re.compile('%{_datadir}/man([/\s$])') +re_infodir = re.compile('%{_datadir}/info([/\s$])') + + +def strip_useless_spaces(s): + return ' '.join(s.split()) + + +def replace_known_dirs(s): + s = s.replace('%_prefix', '%{_prefix}') + s = s.replace('%_bindir', '%{_bindir}') + s = s.replace('%_sbindir', '%{_sbindir}') + s = s.replace('%_includedir', '%{_includedir}') + s = s.replace('%_datadir', '%{_datadir}') + s = s.replace('%_mandir', '%{_mandir}') + s = s.replace('%_infodir', '%{_infodir}') + s = s.replace('%_libdir', '%{_libdir}') + s = s.replace('%_libexecdir', '%{_libexecdir}') + s = s.replace('%_lib', '%{_lib}') + s = s.replace('%{_prefix}/%{_lib}', '%{_libdir}') + s = s.replace('%_sysconfdir', '%{_sysconfdir}') + s = s.replace('%_localstatedir', '%{_localstatedir}') + s = s.replace('%_initddir', '%{_initddir}') + # old typo in rpm macro + s = s.replace('%_initrddir', '%{_initddir}') + s = s.replace('%{_initrddir}', '%{_initddir}') + + s = re_bindir.sub(r'%{_bindir}\1', s) + s = re_sbindir.sub(r'%{_sbindir}\1', s) + s = re_includedir.sub(r'%{_includedir}\1', s) + s = re_datadir.sub(r'%{_datadir}\1', s) + s = re_mandir.sub(r'%{_mandir}\1', s) + s = re_infodir.sub(r'%{_infodir}\1', s) + + return s + + +def replace_buildroot(s): + s = s.replace('${RPM_BUILD_ROOT}', '%{buildroot}') + s = s.replace('$RPM_BUILD_ROOT', '%{buildroot}') + s = s.replace('%buildroot', '%{buildroot}') + s = s.replace('%{buildroot}/usr', '%{buildroot}%{_prefix}') + s = s.replace('%{buildroot}/', '%{buildroot}') + s = s.replace('%{buildroot}etc/init.d/', '%{buildroot}%{_initddir}/') + s = s.replace('%{buildroot}etc/', '%{buildroot}%{_sysconfdir}/') + s = s.replace('%{buildroot}usr/', '%{buildroot}%{_prefix}/') + s = s.replace('%{buildroot}var/', '%{buildroot}%{_localstatedir}/') + s = s.replace('"%{buildroot}"', '%{buildroot}') + return s + + +def replace_optflags(s): + s = s.replace('${RPM_OPT_FLAGS}', '%{optflags}') + s = s.replace('$RPM_OPT_FLAGS', '%{optflags}') + s = s.replace('%optflags', '%{optflags}') + return s + + +def replace_remove_la(s): + cmp_line = strip_useless_spaces(s) + if cmp_line in [ 'find %{buildroot} -type f -name "*.la" -exec %{__rm} -fv {} +', 'find %{buildroot} -type f -name "*.la" -delete' ]: + s = 'find %{buildroot} -type f -name "*.la" -delete -print' + return s + + +def replace_all(s): + s = replace_buildroot(s) + s = replace_optflags(s) + s = replace_known_dirs(s) + s = replace_remove_la(s) + return s + + +####################################################################### + + +class RpmException(Exception): + pass + + +####################################################################### + + +class RpmSection(object): + ''' + Basic cleanup: we remove trailing spaces. + ''' + + def __init__(self): + self.lines = [] + self.previous_line = None + + def add(self, line): + line = line.rstrip() + line = replace_all(line) + self.lines.append(line) + self.previous_line = line + + def output(self, fout): + for line in self.lines: + fout.write(line + '\n') + + +####################################################################### + + +class RpmCopyright(RpmSection): + ''' + Adds default copyright notice if needed. + Remove initial empty lines. + Remove norootforbuild. + ''' + + + def _add_default_copyright(self): + self.lines.append(time.strftime('''# +# spec file for package +# +# Copyright (c) %Y SUSE LINUX Products GmbH, Nuernberg, Germany. +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via http://bugs.opensuse.org/ +# + +''')) + + + def add(self, line): + if not self.lines and not line: + return + + if line == '# norootforbuild': + return + + RpmSection.add(self, line) + + + def output(self, fout): + if not self.lines: + self._add_default_copyright() + RpmSection.output(self, fout) + + +####################################################################### + + +class RpmPreamble(RpmSection): + ''' + Only keep one empty line for many consecutive ones. + Reorder lines. + Fix bad licenses. + Use one line per BuildRequires/Requires/etc. + Use %{version} instead of %{version}-%{release} for BuildRequires/etc. + Remove AutoReqProv. + Standardize BuildRoot. + + This one is a bit tricky since we reorder things. We have a notion of + paragraphs, categories, and groups. + + A paragraph is a list of non-empty lines. Conditional directives like + %if/%else/%endif also mark paragraphs. It contains categories. + A category is a list of lines on the same topic. It contains a list of + groups. + A group is a list of lines where the first few ones are either %define + or comment lines, and the last one is a normal line. + + This means that the %define and comments will stay attached to one + line, even if we reorder the lines. + ''' + + re_if = re.compile('^\s*(?:%if\s|%ifarch\s|%ifnarch\s|%else\s*$|%endif\s*$)', re.IGNORECASE) + + re_name = re.compile('^Name:\s*(\S*)', re.IGNORECASE) + re_version = re.compile('^Version:\s*(\S*)', re.IGNORECASE) + re_release = re.compile('^Release:\s*(\S*)', re.IGNORECASE) + re_license = re.compile('^License:\s*(.*)', re.IGNORECASE) + re_summary = re.compile('^Summary:\s*(.*)', re.IGNORECASE) + re_url = re.compile('^Url:\s*(\S*)', re.IGNORECASE) + re_group = re.compile('^Group:\s*(.*)', re.IGNORECASE) + re_source = re.compile('^Source(\d*):\s*(\S*)', re.IGNORECASE) + re_patch = re.compile('^((?:#[#\s]*)?)Patch(\d*):\s*(\S*)', re.IGNORECASE) + re_buildrequires = re.compile('^BuildRequires:\s*(.*)', re.IGNORECASE) + re_prereq = re.compile('^PreReq:\s*(.*)', re.IGNORECASE) + re_requires = re.compile('^Requires:\s*(.*)', re.IGNORECASE) + re_recommends = re.compile('^Recommends:\s*(.*)', re.IGNORECASE) + re_suggests = re.compile('^Suggests:\s*(.*)', re.IGNORECASE) + re_supplements = re.compile('^Supplements:\s*(.*)', re.IGNORECASE) + re_provides = re.compile('^Provides:\s*(.*)', re.IGNORECASE) + re_obsoletes = re.compile('^Obsoletes:\s*(.*)', re.IGNORECASE) + re_buildroot = re.compile('^\s*BuildRoot:', re.IGNORECASE) + re_buildarch = re.compile('^\s*BuildArch:\s*(.*)', re.IGNORECASE) + + re_requires_token = re.compile('(\s*(\S+(?:\s*(?:[<>]=?|=)\s*[^\s,]+)?),?)') + + category_to_re = { + 'name': re_name, + 'version': re_version, + 'release': re_release, + 'license': re_license, + 'summary': re_summary, + 'url': re_url, + 'group': re_group, + # for source, we have a special match to keep the source number + # for patch, we have a special match to keep the patch number + 'buildrequires': re_buildrequires, + 'prereq': re_prereq, + 'requires': re_requires, + 'recommends': re_recommends, + 'suggests': re_suggests, + 'supplements': re_supplements, + # for provides/obsoletes, we have a special case because we group them + # for build root, we have a special match because we force its value + 'buildarch': re_buildarch + } + + category_to_key = { + 'name': 'Name', + 'version': 'Version', + 'release': 'Release', + 'license': 'License', + 'summary': 'Summary', + 'url': 'Url', + 'group': 'Group', + 'source': 'Source', + 'patch': 'Patch', + 'buildrequires': 'BuildRequires', + 'prereq': 'PreReq', + 'requires': 'Requires', + 'recommends': 'Recommends', + 'suggests': 'Suggests', + 'supplements': 'Supplements', + # Provides/Obsoletes cannot be part of this since we want to keep them + # mixed, so we'll have to specify the key when needed + 'buildroot': 'BuildRoot', + 'buildarch': 'BuildArch' + } + + category_to_fixer = { + } + + license_fixes = { + 'LGPL v2.0 only': 'LGPLv2.0', + 'LGPL v2.0 or later': 'LGPLv2.0+', + 'LGPL v2.1 only': 'LGPLv2.1', + 'LGPL v2.1 or later': 'LGPLv2.1+', + 'LGPL v3 only': 'LGPLv3', + 'LGPL v3 or later': 'LGPLv3+', + 'GPL v2 only': 'GPLv2', + 'GPL v2 or later': 'GPLv2+', + 'GPL v3 only': 'GPLv3', + 'GPL v3 or later': 'GPLv3+' + } + + categories_order = [ 'name', 'version', 'release', 'license', 'summary', 'url', 'group', 'source', 'patch', 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements', 'provides_obsoletes', 'buildroot', 'buildarch', 'misc' ] + + categories_with_sorted_package_tokens = [ 'buildrequires', 'prereq', 'requires', 'recommends', 'suggests', 'supplements' ] + categories_with_package_tokens = categories_with_sorted_package_tokens[:] + categories_with_package_tokens.append('provides_obsoletes') + + re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE) + + + def __init__(self): + RpmSection.__init__(self) + self._start_paragraph() + + + def _start_paragraph(self): + self.paragraph = {} + for i in self.categories_order: + self.paragraph[i] = [] + self.current_group = [] + + + def _add_group(self, group): + t = type(group) + + if t == str: + RpmSection.add(self, group) + elif t == list: + for subgroup in group: + self._add_group(subgroup) + else: + raise RpmException('Unknown type of group in preamble: %s' % t) + + + def _end_paragraph(self): + def sort_helper_key(a): + t = type(a) + if t == str: + return a + elif t == list: + return a[-1] + else: + raise RpmException('Unknown type during sort: %s' % t) + + for i in self.categories_order: + if i in self.categories_with_sorted_package_tokens: + self.paragraph[i].sort(key=sort_helper_key) + for group in self.paragraph[i]: + self._add_group(group) + if self.current_group: + # the current group was not added to any category. It's just some + # random stuff that should be at the end anyway. + self._add_group(self.current_group) + + self._start_paragraph() + + + def _fix_license(self, value): + licenses = value.split(';') + for (index, license) in enumerate(licenses): + license = strip_useless_spaces(license) + if self.license_fixes.has_key(license): + license = self.license_fixes[license] + licenses[index] = license + + return [ ' ; '.join(licenses) ] + + category_to_fixer['license'] = _fix_license + + + def _fix_list_of_packages(self, value): + if self.re_requires_token.match(value): + tokens = [ item[1] for item in self.re_requires_token.findall(value) ] + for (index, token) in enumerate(tokens): + token = token.replace('%{version}-%{release}', '%{version}') + tokens[index] = token + + tokens.sort() + return tokens + else: + return [ value ] + + for i in categories_with_package_tokens: + category_to_fixer[i] = _fix_list_of_packages + + + def _add_line_value_to(self, category, value, key = None): + """ + Change a key-value line, to make sure we have the right spacing. + + Note: since we don't have a key <-> category matching, we need to + redo one. (Eg: Provides and Obsoletes are in the same category) + """ + keylen = len('BuildRequires: ') + + if key: + pass + elif self.category_to_key.has_key(category): + key = self.category_to_key[category] + else: + raise RpmException('Unhandled category in preamble: %s' % category) + + key += ':' + while len(key) < keylen: + key += ' ' + + if self.category_to_fixer.has_key(category): + values = self.category_to_fixer[category](self, value) + else: + values = [ value ] + + for value in values: + line = key + value + self._add_line_to(category, line) + + + def _add_line_to(self, category, line): + if self.current_group: + self.current_group.append(line) + self.paragraph[category].append(self.current_group) + self.current_group = [] + else: + self.paragraph[category].append(line) + + self.previous_line = line + + + def add(self, line): + if len(line) == 0: + if not self.previous_line or len(self.previous_line) == 0: + return + + # we put the empty line in the current group (so we don't list it), + # and write the paragraph + self.current_group.append(line) + self._end_paragraph() + self.previous_line = line + return + + elif self.re_if.match(line): + # %if/%else/%endif marks the end of the previous paragraph + # We append the line at the end of the previous paragraph, though, + # since it will stay at the end there. If putting it at the + # beginning of the next paragraph, it will likely move (with the + # misc category). + self.current_group.append(line) + self._end_paragraph() + self.previous_line = line + return + + elif re_comment.match(line) or re_define.match(line): + self.current_group.append(line) + self.previous_line = line + return + + elif self.re_autoreqprov.match(line): + return + + elif self.re_source.match(line): + match = self.re_source.match(line) + self._add_line_value_to('source', match.group(2), key = 'Source%s' % match.group(1)) + return + + elif self.re_patch.match(line): + # FIXME: this is not perfect, but it's good enough for most cases + if not self.previous_line or not re_comment.match(self.previous_line): + self.current_group.append('# PATCH-MISSING-TAG -- See http://en.opensuse.org/Packaging/Patches') + + match = self.re_patch.match(line) + self._add_line_value_to('source', match.group(3), key = '%sPatch%s' % (match.group(1), match.group(2))) + return + + elif self.re_provides.match(line): + match = self.re_provides.match(line) + self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Provides') + return + + elif self.re_obsoletes.match(line): + match = self.re_obsoletes.match(line) + self._add_line_value_to('provides_obsoletes', match.group(1), key = 'Obsoletes') + return + + elif self.re_buildroot.match(line): + if len(self.paragraph['buildroot']) == 0: + self._add_line_value_to('buildroot', '%{_tmppath}/%{name}-%{version}-build') + return + + else: + for (category, regexp) in self.category_to_re.iteritems(): + match = regexp.match(line) + if match: + self._add_line_value_to(category, match.group(1)) + return + + self._add_line_to('misc', line) + + + def output(self, fout): + self._end_paragraph() + RpmSection.output(self, fout) + + +####################################################################### + + +class RpmPackage(RpmPreamble): + ''' + We handle this the same was as the preamble. + ''' + + def add(self, line): + # The first line (%package) should always be added and is different + # from the lines we handle in RpmPreamble. + if self.previous_line is None: + RpmSection.add(self, line) + return + + RpmPreamble.add(self, line) + + +####################################################################### + + +class RpmDescription(RpmSection): + ''' + Only keep one empty line for many consecutive ones. + Remove Authors from description. + ''' + + def __init__(self): + RpmSection.__init__(self) + self.removing_authors = False + # Tracks the use of a macro. When this happens and we're still in a + # description, we actually don't know where we are so we just put all + # the following lines blindly, without trying to fix anything. + self.unknown_line = False + + def add(self, line): + lstrip = line.lstrip() + if self.previous_line != None and len(lstrip) > 0 and lstrip[0] == '%': + self.unknown_line = True + + if self.removing_authors and not self.unknown_line: + return + + if len(line) == 0: + if not self.previous_line or len(self.previous_line) == 0: + return + + if line == 'Authors:': + self.removing_authors = True + return + + RpmSection.add(self, line) + + +####################################################################### + + +class RpmPrep(RpmSection): + ''' + Try to simplify to %setup -q when possible. + ''' + + def add(self, line): + if line.startswith('%setup'): + cmp_line = line.replace(' -q', '') + cmp_line = cmp_line.replace(' -n %{name}-%{version}', '') + cmp_line = strip_useless_spaces(cmp_line) + if cmp_line == '%setup': + line = '%setup -q' + + RpmSection.add(self, line) + + +####################################################################### + + +class RpmBuild(RpmSection): + ''' + Replace %{?jobs:-j%jobs} (suse-ism) with %{?_smp_mflags} + ''' + + def add(self, line): + if not re_comment.match(line): + line = line.replace('%{?jobs:-j%jobs}' , '%{?_smp_mflags}') + line = line.replace('%{?jobs: -j%jobs}', '%{?_smp_mflags}') + + RpmSection.add(self, line) + + +####################################################################### + + +class RpmInstall(RpmSection): + ''' + Remove commands that wipe out the build root. + Use %makeinstall macro. + ''' + + re_autoreqprov = re.compile('^\s*AutoReqProv:\s*on\s*$', re.IGNORECASE) + + def add(self, line): + # remove double spaces when comparing the line + cmp_line = strip_useless_spaces(line) + cmp_line = replace_buildroot(cmp_line) + + if cmp_line.find('DESTDIR=%{buildroot}') != -1: + buf = cmp_line.replace('DESTDIR=%{buildroot}', '') + buf = strip_useless_spaces(buf) + if buf == 'make install': + line = '%makeinstall' + elif cmp_line == 'rm -rf %{buildroot}': + return + + if self.re_autoreqprov.match(line): + return + + RpmSection.add(self, line) + + +####################################################################### + + +class RpmClean(RpmSection): + pass + + +####################################################################### + + +class RpmScriptlets(RpmSection): + ''' + Do %post -p /sbin/ldconfig when possible. + ''' + + def __init__(self): + RpmSection.__init__(self) + self.cache = [] + + + def add(self, line): + if len(self.lines) == 0: + if not self.cache: + if line.find(' -p ') == -1 and line.find(' -f ') == -1: + self.cache.append(line) + return + else: + if line in ['', '/sbin/ldconfig' ]: + self.cache.append(line) + return + else: + for cached in self.cache: + RpmSection.add(self, cached) + self.cache = None + + RpmSection.add(self, line) + + + def output(self, fout): + if self.cache: + RpmSection.add(self, self.cache[0] + ' -p /sbin/ldconfig') + RpmSection.add(self, '') + + RpmSection.output(self, fout) + + +####################################################################### + + +class RpmFiles(RpmSection): + """ + Replace additional /usr, /etc and /var because we're sure we can use + macros there. + + Replace '%dir %{_includedir}/mux' and '%{_includedir}/mux/*' with + '%{_includedir}/mux/' + """ + + re_etcdir = re.compile('(^|\s)/etc/') + re_usrdir = re.compile('(^|\s)/usr/') + re_vardir = re.compile('(^|\s)/var/') + + re_dir = re.compile('^\s*%dir\s*(\S+)\s*') + + def __init__(self): + RpmSection.__init__(self) + self.dir_on_previous_line = None + + + def add(self, line): + line = self.re_etcdir.sub(r'\1%{_sysconfdir}/', line) + line = self.re_usrdir.sub(r'\1%{_prefix}/', line) + line = self.re_vardir.sub(r'\1%{_localstatedir}/', line) + + if self.dir_on_previous_line: + if line == self.dir_on_previous_line + '/*': + RpmSection.add(self, self.dir_on_previous_line + '/') + self.dir_on_previous_line = None + return + else: + RpmSection.add(self, '%dir ' + self.dir_on_previous_line) + self.dir_on_previous_line = None + + match = self.re_dir.match(line) + if match: + self.dir_on_previous_line = match.group(1) + return + + RpmSection.add(self, line) + + +####################################################################### + + +class RpmChangelog(RpmSection): + ''' + Remove changelog entries. + ''' + + def add(self, line): + # only add the first line (%changelog) + if len(self.lines) == 0: + RpmSection.add(self, line) + + +####################################################################### + + +class RpmSpecCleaner: + + specfile = None + fin = None + fout = None + current_section = None + + + re_spec_package = re.compile('^%package\s*', re.IGNORECASE) + re_spec_description = re.compile('^%description\s*', re.IGNORECASE) + re_spec_prep = re.compile('^%prep\s*$', re.IGNORECASE) + re_spec_build = re.compile('^%build\s*$', re.IGNORECASE) + re_spec_install = re.compile('^%install\s*$', re.IGNORECASE) + re_spec_clean = re.compile('^%clean\s*$', re.IGNORECASE) + re_spec_scriptlets = re.compile('(?:^%pretrans\s*)|(?:^%pre\s*)|(?:^%post\s*)|(?:^%preun\s*)|(?:^%postun\s*)|(?:^%posttrans\s*)', re.IGNORECASE) + re_spec_files = re.compile('^%files\s*', re.IGNORECASE) + re_spec_changelog = re.compile('^%changelog\s*$', re.IGNORECASE) + + + section_starts = [ + (re_spec_package, RpmPackage), + (re_spec_description, RpmDescription), + (re_spec_prep, RpmPrep), + (re_spec_build, RpmBuild), + (re_spec_install, RpmInstall), + (re_spec_clean, RpmClean), + (re_spec_scriptlets, RpmScriptlets), + (re_spec_files, RpmFiles), + (re_spec_changelog, RpmChangelog) + ] + + + def __init__(self, specfile, output, inline, force): + if not specfile.endswith('.spec'): + raise RpmException('%s does not appear to be a spec file.' % specfile) + + if not os.path.exists(specfile): + raise RpmException('%s does not exist.' % specfile) + + self.specfile = specfile + self.output = output + self.inline = inline + + self.fin = open(self.specfile) + + if self.output: + if not force and os.path.exists(self.output): + raise RpmException('%s already exists.' % self.output) + self.fout = open(self.output, 'w') + elif self.inline: + io = cStringIO.StringIO() + while True: + bytes = self.fin.read(500 * 1024) + if len(bytes) == 0: + break + io.write(bytes) + + self.fin.close() + io.seek(0) + self.fin = io + self.fout = open(self.specfile, 'w') + else: + self.fout = sys.stdout + + + def run(self): + if not self.specfile or not self.fin: + raise RpmException('No spec file.') + + def _line_for_new_section(self, line): + if isinstance(self.current_section, RpmCopyright): + if not re_comment.match(line): + return RpmPreamble + + for (regexp, newclass) in self.section_starts: + if regexp.match(line): + return newclass + + return None + + + self.current_section = RpmCopyright() + + while True: + line = self.fin.readline() + if len(line) == 0: + break + # Remove \n to make it easier to parse things + line = line[:-1] + + new_class = _line_for_new_section(self, line) + if new_class: + self.current_section.output(self.fout) + self.current_section = new_class() + + self.current_section.add(line) + + self.current_section.output(self.fout) + + + def __del__(self): + if self.fin: + self.fin.close() + self.fin = None + if self.fout: + self.fout.close() + self.fout = None + + +####################################################################### + + +def main(args): + 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.') + + parser.add_option("-i", "--inline", action="store_true", dest="inline", + default=False, help="edit the file inline") + parser.add_option("-o", "--output", dest="output", + help="output file") + parser.add_option("-f", "--force", action="store_true", dest="force", + default=False, help="overwrite output file if already existing") + parser.add_option("-v", "--version", action="store_true", dest="version", + default=False, help="display version (" + VERSION + ")") + + (options, args) = parser.parse_args() + + if options.version: + print 'spec-cleaner ' + VERSION + return 0 + + if len(args) != 1: + print >> sys.stderr, '\nUsage:\n\tspec-cleaner file.spec\n' + return 1 + + spec = os.path.expanduser(args[0]) + if options.output: + options.output = os.path.expanduser(options.output) + + if options.output == spec: + options.output = '' + options.inline = True + + if options.output and options.inline: + print >> sys.stderr, 'Conflicting options: --inline and --output.' + return 1 + + try: + cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force) + cleaner.run() + except RpmException, e: + print >> sys.stderr, '%s' % e + return 1 + + return 0 + +if __name__ == '__main__': + try: + res = main(sys.argv) + sys.exit(res) + except KeyboardInterrupt: + pass diff --git a/spec-cleaner.changes b/spec-cleaner.changes new file mode 100644 index 0000000..65247ba --- /dev/null +++ b/spec-cleaner.changes @@ -0,0 +1,5 @@ +------------------------------------------------------------------- +Thu Sep 30 16:04:03 CEST 2010 - vuntz@opensuse.org + +- Initial package (version 0.1) + diff --git a/spec-cleaner.spec b/spec-cleaner.spec new file mode 100644 index 0000000..c62f2f1 --- /dev/null +++ b/spec-cleaner.spec @@ -0,0 +1,49 @@ +# +# spec file for package spec-cleaner (version 0.1) +# +# Copyright (c) 2010 Vincent Untz +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via http://bugs.opensuse.org/ +# + +Name: spec-cleaner +Version: 0.1 +Release: 1 +License: BSD 3-Clause +Summary: .spec file cleaner +Url: http://gitorious.org/opensuse/spec-cleaner +Group: Development/Tools/Other +Source0: spec-cleaner +Requires: python-base +BuildRoot: %{_tmppath}/%{name}-%{version}-build +BuildArch: noarch + +%description +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. + +%prep + +%build + +%install +install -D -m0755 %{S:0} %{buildroot}%{_bindir}/spec-cleaner + +%clean +%{__rm} -rf %{buildroot} + +%files +%defattr(-, root, root) +%{_bindir}/spec-cleaner + +%changelog