#!/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 <ORGANIZATION> 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 <vuntz@novell.com>
#   Pavol Rusnak <prusnak@opensuse.org>
#   Petr Uzel <petr.uzel@suse.cz>
#

import os
import sys

import cStringIO
import optparse
import re
import time
import tempfile
import subprocess
import shlex

#######################################################################

VERSION = '0.2'

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('%_usr', '%{_prefix}')
    s = s.replace('%{_usr}', '%{_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('%_var', '%{_localstatedir}')
    s = s.replace('%{_var}', '%{_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}/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_utils(s):
    # take care of all utilities macros that bloat spec file
    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', }
    for i in r:
      s = s.replace('%__' + i, r[i])
      s = s.replace('%{__' + i + '}', r[i])

    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', ]:
        s = s.replace('%__' + i, i)
        s = s.replace('%{__' + i + '}', i)

    return s


def replace_buildservice(s):
    for i in ['centos', 'debian', 'fedora', 'mandriva', 'meego', 'rhel', 'sles', 'suse', 'ubuntu']:
        s = s.replace('%' + i + '_version', '0%{?' + i + '_version}').replace('00%{','0%{')
        s = s.replace('%{' + i + '_version}', '0%{?' + i + '_version}').replace('00%{','0%{')
    return s

def replace_preamble_macros(s):
    for i in ['name', 'version', 'release']:
        s = s.replace('%' + i, '%{' + i + '}')
    for i in map(str,range(100)):
        s = s.replace('%{P:' + i + '}', '%{PATCH' + i + '}')
        s = s.replace('%PATCH' + i, '%{PATCH' + i + '}')
        s = s.replace('%{S:' + i + '}', '%{SOURCE' + i + '}')
        s = s.replace('%SOURCE' + i, '%{SOURCE' + i + '}')
    return s

def replace_all(s):
    s = replace_buildroot(s)
    s = replace_optflags(s)
    s = replace_known_dirs(s)
    s = replace_remove_la(s)
    s = replace_utils(s)
    s = replace_buildservice(s)
    s = replace_preamble_macros(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('''#
# Please submit bugfixes or comments via http://bugs.opensuse.org/
#
'''))


    def add(self, line):
        if not self.lines and not line:
            return

        if line.startswith('# norootforbuild') or \
           line.startswith('# usedforbuild'):
            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': 'Requires(pre)',
        '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+',
        'FDL 1.1': 'FDLv1.1',
        'FDL 1.2': 'FDLv1.2',
        'FDL 1.2 or later': 'FDLv1.2+',
        'FDL 1.3': 'FDLv1.3'
    }

    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:
                key = a
            elif t == list:
                key = a[-1]
            else:
                raise RpmException('Unknown type during sort: %s' % t)

            # Put pkgconfig()-style packages at the end of the list, after all
            # non-pkgconfig()-style packages
            if key.find('pkgconfig(') != -1:
                return '1'+key
            else:
                return '0'+key

        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 _pkgname_to_pkgconfig(self, value):
        r = {
          'cairo-devel': 'cairo',
          'dbus-1-devel': 'dbus-1',
          'dbus-1-glib-devel': 'dbus-glib-1',
          'gconf2-devel': 'gconf-2.0',
          'gstreamer-0_10-devel': 'gstreamer-0.10',
          'exo-devel': 'exo-1',
          'glib2-devel': 'glib-2.0',
          'gtk2-devel': 'gtk+-2.0',
          'hal-devel': 'hal',
          'ImageMagick-devel': 'ImageMagick',
          'libapr1-devel': 'apr-1',
          'libapr-util1-devel': 'apr-util-1',
          'libexif-devel': 'libexif',
          'libgarcon-devel': 'garcon-1',
          'libglade2-devel': 'libglade-2.0',
          'libgladeui-1_0-devel': 'gladeui-1.0',
          'libgudev-1_0-devel': 'gudev-1.0',
          'libical-devel': 'libical',
          'libnotify-devel': 'libnotify',
          'libwnck-devel': 'libwnck-1.0',
          'libxfce4ui-devel': 'libxfce4ui-1',
          'libxfce4util-devel': 'libxfce4util-1.0',
          'libxfcegui4-devel': 'libxfcegui4-1.0',
          'libxfconf-devel': 'libxfconf-0',
          'libxklavier-devel': 'libxklavier',
          'libxml2-devel': 'libxml-2.0',
          'pango-devel': 'pango',
          'startup-notification-devel': 'libstartup-notification-1.0',
          'vte-devel': 'vte',
          'xfce4-panel-devel': 'libxfce4panel-1.0',
        }
        for i in r:
            value = value.replace(i, 'pkgconfig('+r[i]+')')
        return value

    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}')
                token = token.replace(' ','')
                token = re.sub(r'([<>]=?|=)', r' \1 ', token)
                token = self._pkgname_to_pkgconfig(token)
                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://wiki.opensuse.org/openSUSE:Packaging_Patches_guidelines')

            match = self.re_patch.match(line)
            # convert Patch: to Patch0:
            if match.group(2) == '':
                zero = '0'
            else:
                zero = ''
            self._add_line_value_to('source', match.group(3), key = '%sPatch%s%s' % (match.group(1), zero, 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.
        Replace %patch with %patch0
    '''

    re_patch = re.compile('^%patch\s*(.*)-P\s*(\d*)\s*(.*)')

    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'
        if self.re_patch.match(line):
            match = self.re_patch.match(line)
            line = strip_useless_spaces('%%patch%s %s %s' % (match.group(2), match.group(1), match.group(3)))
        elif line.startswith('%patch ') or line == '%patch':
            line = line.replace('%patch','%patch0')

        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('%_smp_mflags'       , '%{?_smp_mflags}')
            line = line.replace('%{_smp_mflags}'     , '%{?_smp_mflags}')
            line = line.replace('%{?jobs:-j%jobs}'   , '%{?_smp_mflags}')
            line = line.replace('%{?jobs: -j%jobs}'  , '%{?_smp_mflags}')
            line = line.replace('%{?jobs:-j %jobs}'  , '%{?_smp_mflags}')
            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 %make_install macro.
        Replace %makeinstall (suse-ism).
    '''

    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' or buf == 'make  install':
                line = '%make_install'
        elif cmp_line == '%makeinstall':
            line = '%make_install'
        elif cmp_line == 'rm -rf %{buildroot}':
            return

        RpmSection.add(self, line)


#######################################################################


class RpmClean(RpmSection):
    # if the section contains just rm -rf %{buildroot} then remove the whole section (including %clean)
    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, diff, diff_prog):
        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.diff = diff
        self.diff_prog = diff_prog

        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')
        elif self.diff:
            self.fout = tempfile.NamedTemporaryFile(prefix=self.specfile+'.', suffix='.spec')
        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)
        self.fout.flush()

        if self.diff:
            cmd = shlex.split(self.diff_prog + " " + self.specfile.replace(" ","\\ ") + " " + self.fout.name.replace(" ","\\ "))
            try:
                subprocess.call(cmd, shell=False)
            except OSError as e:
                raise RpmException('Could not execute %s (%s)' % (self.diff_prog.split()[0], e.strerror))

    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(
    usage='usage: %prog [options] package.spec',
    epilog='''This script cleans the given spec file according to some arbitrary style guide and prints the result.
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("-d", "--diff", action="store_true", dest="diff",
                      default=False, help="call external program to compare new and original specfile")
    parser.add_option("--diff-prog", dest="diff_prog",
                      help="program to generate diff (implies --diff)")
    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:
        parser.print_help()
        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.diff_prog:
        # --diff-prog implies -d
        options.diff = True
    else:
        # if diff-prog is not specified, set default here
        options.diff_prog = "vimdiff"

    if options.output and options.inline:
        print >> sys.stderr,  'Conflicting options: --inline and --output.'
        return 1

    if options.diff and options.output:
        print >> sys.stderr,  'Conflicting options: --diff and --output.'
        return 1

    if options.diff and options.inline:
        print >> sys.stderr,  'Conflicting options: --diff and --inline.'
        return 1

    try:
        cleaner = RpmSpecCleaner(spec, options.output, options.inline, options.force, options.diff, options.diff_prog)
        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
