Introduce rpm helpers
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>
Thu, 12 Jan 2012 13:29:03 +0000 (15:29 +0200)
committerGuido Günther <agx@sigxcpu.org>
Thu, 24 Jul 2014 21:33:36 +0000 (23:33 +0200)
Implements a new gbp.rpm module that contains functionality for e.g.
parsing and editing spec files, reading src.rpm files rpm-specific
packaging policy etc.

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
Signed-off-by: Ed Bartosh <eduard.bartosh@intel.com>
Signed-off-by: Zhang Qiang <qiang.z.zhang@intel.com>
Signed-off-by: Huang Hao <hao.h.huang@intel.com>
30 files changed:
gbp/rpm/__init__.py [new file with mode: 0644]
gbp/rpm/git.py [new file with mode: 0644]
gbp/rpm/lib_rpm.py [new file with mode: 0644]
gbp/rpm/linkedlist.py [new file with mode: 0644]
gbp/rpm/policy.py [new file with mode: 0644]
tests/20_test_rpm.py [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/bar.tar.gz [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/foo.txt [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/gbp-test-1.0.tar.bz2 [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/gbp-test-native-1.0.zip [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/gbp-test2-3.0.tar.gz [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/my.patch [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/my2.patch [new file with mode: 0644]
tests/data/rpm/rpmbuild/SOURCES/my3.patch [new file with mode: 0644]
tests/data/rpm/rpmbuild/SPECS/gbp-test-native.spec [new file with mode: 0644]
tests/data/rpm/rpmbuild/SPECS/gbp-test-native2.spec [new file with mode: 0644]
tests/data/rpm/rpmbuild/SPECS/gbp-test.spec [new file with mode: 0644]
tests/data/rpm/rpmbuild/SPECS/gbp-test2.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test-native.spec [new symlink]
tests/data/rpm/specs/gbp-test-native2.spec [new symlink]
tests/data/rpm/specs/gbp-test-quirks.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test-reference.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test-reference2.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test-tags.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test-updates-reference.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test-updates.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test.spec [new symlink]
tests/data/rpm/specs/gbp-test2-reference.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test2-reference2.spec [new file with mode: 0644]
tests/data/rpm/specs/gbp-test2.spec [new symlink]

diff --git a/gbp/rpm/__init__.py b/gbp/rpm/__init__.py
new file mode 100644 (file)
index 0000000..87f82ff
--- /dev/null
@@ -0,0 +1,962 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2006,2007 Guido Guenther <agx@sigxcpu.org>
+# (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""provides some rpm source package related helpers"""
+
+import commands
+import sys
+import os
+import re
+import tempfile
+import glob
+import shutil as shutil
+from optparse import OptionParser
+from collections import defaultdict
+
+import gbp.command_wrappers as gbpc
+from gbp.errors import GbpError
+from gbp.git import GitRepositoryError
+from gbp.patch_series import (PatchSeries, Patch)
+import gbp.log
+from gbp.pkg import (UpstreamSource, compressor_opts, parse_archive_filename)
+from gbp.rpm.policy import RpmPkgPolicy
+from gbp.rpm.linkedlist import LinkedList
+from gbp.rpm.lib_rpm import librpm, get_librpm_log
+
+
+class NoSpecError(Exception):
+    """Spec file parsing error"""
+    pass
+
+class MacroExpandError(Exception):
+    """Macro expansion in spec file failed"""
+    pass
+
+
+class RpmUpstreamSource(UpstreamSource):
+    """Upstream source class for RPM packages"""
+    def __init__(self, name, unpacked=None, **kwargs):
+        super(RpmUpstreamSource, self).__init__(name,
+                                                unpacked,
+                                                RpmPkgPolicy,
+                                                **kwargs)
+
+
+class SrcRpmFile(object):
+    """Keeps all needed data read from a source rpm"""
+    def __init__(self, srpmfile):
+        # Do not required signed packages to be able to import
+        ts_vsflags = (librpm.RPMVSF_NOMD5HEADER | librpm.RPMVSF_NORSAHEADER |
+                      librpm.RPMVSF_NOSHA1HEADER | librpm.RPMVSF_NODSAHEADER |
+                      librpm.RPMVSF_NOMD5 | librpm.RPMVSF_NORSA |
+                      librpm.RPMVSF_NOSHA1 | librpm.RPMVSF_NODSA)
+        srpmfp = open(srpmfile)
+        self.rpmhdr = librpm.ts(vsflags=ts_vsflags).hdrFromFdno(srpmfp.fileno())
+        srpmfp.close()
+        self.srpmfile = os.path.abspath(srpmfile)
+
+    @property
+    def version(self):
+        """Get the (downstream) version of the RPM package"""
+        version = dict(upstreamversion = self.rpmhdr[librpm.RPMTAG_VERSION],
+                       release = self.rpmhdr[librpm.RPMTAG_RELEASE])
+        if self.rpmhdr[librpm.RPMTAG_EPOCH] is not None:
+            version['epoch'] = str(self.rpmhdr[librpm.RPMTAG_EPOCH])
+        return version
+
+    @property
+    def name(self):
+        """Get the name of the RPM package"""
+        return self.rpmhdr[librpm.RPMTAG_NAME]
+
+    @property
+    def upstreamversion(self):
+        """Get the upstream version of the RPM package"""
+        return self.rpmhdr[librpm.RPMTAG_VERSION]
+
+    @property
+    def packager(self):
+        """Get the packager of the RPM package"""
+        return self.rpmhdr[librpm.RPMTAG_PACKAGER]
+
+    def unpack(self, dest_dir):
+        """
+        Unpack the source rpm to tmpdir.
+        Leave the cleanup to the caller in case of an error.
+        """
+        gbpc.RunAtCommand('rpm2cpio',
+                          [self.srpmfile, '|', 'cpio', '-id'],
+                          shell=True)(dir=dest_dir)
+
+
+class SpecFile(object):
+    """Class for parsing/modifying spec files"""
+    tag_re = re.compile(r'^(?P<name>[a-z]+)(?P<num>[0-9]+)?\s*:\s*'
+                         '(?P<value>\S(.*\S)?)\s*$', flags=re.I)
+    directive_re = re.compile(r'^%(?P<name>[a-z]+)(?P<num>[0-9]+)?'
+                               '(\s+(?P<args>.*))?$', flags=re.I)
+    gbptag_re = re.compile(r'^\s*#\s*gbp-(?P<name>[a-z-]+)'
+                            '(\s*:\s*(?P<args>\S.*))?$', flags=re.I)
+    # Here "sections" stand for all scripts, scriptlets and other directives,
+    # but not macros
+    section_identifiers = ('package', 'description', 'prep', 'build', 'install',
+            'clean', 'check', 'pre', 'preun', 'post', 'postun', 'verifyscript',
+            'files', 'changelog', 'triggerin', 'triggerpostin', 'triggerun',
+            'triggerpostun')
+
+    def __init__(self, filename=None, filedata=None):
+
+        self._content = LinkedList()
+
+        # Check args: only filename or filedata can be given, not both
+        if filename is None and filedata is None:
+            raise NoSpecError("No filename or raw data given for parsing!")
+        elif filename and filedata:
+            raise NoSpecError("Both filename and raw data given, don't know "
+                              "which one to parse!")
+        elif filename:
+            # Load spec file into our special data structure
+            self.specfile = os.path.basename(filename)
+            self.specdir = os.path.dirname(os.path.abspath(filename))
+            try:
+                with open(filename) as spec_file:
+                    for line in spec_file.readlines():
+                        self._content.append(line)
+            except IOError as err:
+                raise NoSpecError("Unable to read spec file: %s" % err)
+        else:
+            self.specfile = None
+            self.specdir = None
+            for line in filedata.splitlines():
+                self._content.append(line + '\n')
+
+        # Use rpm-python to parse the spec file content
+        self._filtertags = ("excludearch", "excludeos", "exclusivearch",
+                            "exclusiveos","buildarch")
+        self._listtags = self._filtertags + ('source', 'patch',
+                                  'requires', 'conflicts', 'recommends',
+                                  'suggests', 'supplements', 'enhances',
+                                  'provides', 'obsoletes', 'buildrequires',
+                                  'buildconflicts', 'buildrecommends',
+                                  'buildsuggests', 'buildsupplements',
+                                  'buildenhances', 'collections',
+                                  'nosource', 'nopatch')
+        self._specinfo = self._parse_filtered_spec(self._filtertags)
+
+        # Other initializations
+        source_header = self._specinfo.packages[0].header
+        self.name = source_header[librpm.RPMTAG_NAME]
+        self.upstreamversion = source_header[librpm.RPMTAG_VERSION]
+        self.release = source_header[librpm.RPMTAG_RELEASE]
+        # rpm-python returns epoch as 'long', convert that to string
+        self.epoch = str(source_header[librpm.RPMTAG_EPOCH]) \
+            if source_header[librpm.RPMTAG_EPOCH] != None else None
+        self.packager = source_header[librpm.RPMTAG_PACKAGER]
+        self._tags = {}
+        self._special_directives = defaultdict(list)
+        self._gbp_tags = defaultdict(list)
+
+        # Parse extra info from spec file
+        self._parse_content()
+
+        # Find 'Packager' tag. Needed to circumvent a bug in python-rpm where
+        # spec.sourceHeader[librpm.RPMTAG_PACKAGER] is not reset when a new spec
+        # file is parsed
+        if 'packager' not in self._tags:
+            self.packager = None
+
+        self.orig_src = self._guess_orig_file()
+
+    def _parse_filtered_spec(self, skip_tags):
+        """Parse a filtered spec file in rpm-python"""
+        skip_tags = [tag.lower() for tag in skip_tags]
+        with tempfile.NamedTemporaryFile(prefix='gbp') as filtered:
+            filtered.writelines(str(line) for line in self._content
+                    if str(line).split(":")[0].strip().lower() not in skip_tags)
+            filtered.flush()
+            try:
+                # Parse two times to circumvent a rpm-python problem where
+                # macros are not expanded if used before their definition
+                librpm.spec(filtered.name)
+                return librpm.spec(filtered.name)
+            except ValueError as err:
+                rpmlog = get_librpm_log()
+                gbp.log.debug("librpm log:\n        %s" %
+                                "\n        ".join(rpmlog))
+                raise GbpError("RPM error while parsing %s: %s (%s)" %
+                                (self.specfile, err, rpmlog[-1]))
+
+    @property
+    def version(self):
+        """Get the (downstream) version"""
+        version = dict(upstreamversion = self.upstreamversion,
+                       release = self.release)
+        if self.epoch != None:
+            version['epoch'] = self.epoch
+        return version
+
+    @property
+    def specpath(self):
+        """Get the dir/filename"""
+        return os.path.join(self.specdir, self.specfile)
+
+    @property
+    def ignorepatches(self):
+        """Get numbers of ignored patches as a sorted list"""
+        if 'ignore-patches' in self._gbp_tags:
+            data = self._gbp_tags['ignore-patches'][-1]['args'].split()
+            return sorted([int(num) for num in data])
+        return []
+
+    def _patches(self):
+        """Get all patch tags as a dict"""
+        if 'patch' not in self._tags:
+            return {}
+        return {patch['num']: patch for patch in self._tags['patch']['lines']}
+
+    def _sources(self):
+        """Get all source tags as a dict"""
+        if 'source' not in self._tags:
+            return {}
+        return {src['num']: src for src in self._tags['source']['lines']}
+
+    def sources(self):
+        """Get all source tags as a dict"""
+        return {src['num']: src['linevalue']
+                for src in self._sources().values()}
+
+    def _macro_replace(self, matchobj):
+        macro_dict = {'name': self.name,
+                      'version': self.upstreamversion,
+                      'release': self.release}
+
+        if matchobj.group(2) in macro_dict:
+            return macro_dict[matchobj.group(2)]
+        raise MacroExpandError("Unknown macro '%s'" % matchobj.group(0))
+
+    def macro_expand(self, text):
+        """
+        Expand the rpm macros (that gbp knows of) in the given text.
+
+        @param text: text to check for macros
+        @type text: C{str}
+        @return: text with macros expanded
+        @rtype: C{str}
+        """
+        # regexp to match '%{macro}' and '%macro'
+        macro_re = re.compile(r'%({)?(?P<macro_name>[a-z_][a-z0-9_]*)(?(1)})', flags=re.I)
+        return macro_re.sub(self._macro_replace, text)
+
+    def write_spec_file(self):
+        """
+        Write, possibly updated, spec to disk
+        """
+        with open(os.path.join(self.specdir, self.specfile), 'w') as spec_file:
+            for line in self._content:
+                spec_file.write(str(line))
+
+    def _parse_tag(self, lineobj):
+        """Parse tag line"""
+
+        line = str(lineobj)
+
+        matchobj = self.tag_re.match(line)
+        if not matchobj:
+            return False
+
+        tagname = matchobj.group('name').lower()
+        tagnum = int(matchobj.group('num')) if matchobj.group('num') else None
+        # 'Source:' tags
+        if tagname == 'source':
+            tagnum = 0 if tagnum is None else tagnum
+        # 'Patch:' tags
+        elif tagname == 'patch':
+            tagnum = -1 if tagnum is None else tagnum
+
+        # Record all tag locations
+        try:
+            header = self._specinfo.packages[0].header
+            tagvalue = header[getattr(librpm, 'RPMTAG_%s' % tagname.upper())]
+        except AttributeError:
+            tagvalue = None
+        # We don't support "multivalue" tags like "Provides:" or "SourceX:"
+        # Rpm python doesn't support many of these, thus the explicit list
+        if type(tagvalue) is int or type(tagvalue) is long:
+            tagvalue = str(tagvalue)
+        elif type(tagvalue) is list or tagname in self._listtags:
+            tagvalue = None
+        elif not tagvalue:
+            # Rpm python doesn't give the following, for reason or another
+            if tagname not in ('buildroot', 'autoprov', 'autoreq',
+                               'autoreqprov') + self._filtertags:
+                gbp.log.warn("BUG: '%s:' tag not found by rpm" % tagname)
+            tagvalue = matchobj.group('value')
+        linerecord = {'line': lineobj,
+                      'num': tagnum,
+                      'linevalue': matchobj.group('value')}
+        if tagname in self._tags:
+            self._tags[tagname]['value'] = tagvalue
+            self._tags[tagname]['lines'].append(linerecord)
+        else:
+            self._tags[tagname] = {'value': tagvalue, 'lines': [linerecord]}
+
+        return tagname
+
+    @staticmethod
+    def _patch_macro_opts(args):
+        """Parse arguments of the '%patch' macro"""
+
+        patchparser = OptionParser()
+        patchparser.add_option("-p", dest="strip")
+        patchparser.add_option("-s", dest="silence")
+        patchparser.add_option("-P", dest="patchnum")
+        patchparser.add_option("-b", dest="backup")
+        patchparser.add_option("-E", dest="removeempty")
+        arglist = args.split()
+        return patchparser.parse_args(arglist)[0]
+
+    @staticmethod
+    def _setup_macro_opts(args):
+        """Parse arguments of the '%setup' macro"""
+
+        setupparser = OptionParser()
+        setupparser.add_option("-n", dest="name")
+        setupparser.add_option("-c", dest="create_dir", action="store_true")
+        setupparser.add_option("-D", dest="no_delete_dir", action="store_true")
+        setupparser.add_option("-T", dest="no_unpack_default",
+                               action="store_true")
+        setupparser.add_option("-b", dest="unpack_before")
+        setupparser.add_option("-a", dest="unpack_after")
+        setupparser.add_option("-q", dest="quiet", action="store_true")
+        arglist = args.split()
+        return setupparser.parse_args(arglist)[0]
+
+    def _parse_directive(self, lineobj):
+        """Parse special directive/scriptlet/macro lines"""
+
+        line = str(lineobj)
+        matchobj = self.directive_re.match(line)
+        if not matchobj:
+            return None
+
+        directivename = matchobj.group('name')
+        # '%patch' macros
+        directiveid = None
+        if directivename == 'patch':
+            opts = self._patch_macro_opts(matchobj.group('args'))
+            if matchobj.group('num'):
+                directiveid = int(matchobj.group('num'))
+            elif opts.patchnum:
+                directiveid = int(opts.patchnum)
+            else:
+                directiveid = -1
+
+        # Record special directive/scriptlet/macro locations
+        if directivename in self.section_identifiers + ('setup', 'patch'):
+            linerecord = {'line': lineobj,
+                          'id': directiveid,
+                          'args': matchobj.group('args')}
+            self._special_directives[directivename].append(linerecord)
+        return directivename
+
+    def _parse_gbp_tag(self, linenum, lineobj):
+        """Parse special git-buildpackage tags"""
+
+        line = str(lineobj)
+        matchobj = self.gbptag_re.match(line)
+        if matchobj:
+            gbptagname = matchobj.group('name').lower()
+            if gbptagname not in ('ignore-patches', 'patch-macros'):
+                gbp.log.info("Found unrecognized Gbp tag on line %s: '%s'" %
+                             (linenum, line))
+            if matchobj.group('args'):
+                args = matchobj.group('args').strip()
+            else:
+                args = None
+            record = {'line': lineobj, 'args': args}
+            self._gbp_tags[gbptagname].append(record)
+            return gbptagname
+
+        return None
+
+    def _parse_content(self):
+        """
+        Go through spec file content line-by-line and (re-)parse info from it
+        """
+        in_preamble = True
+        for linenum, lineobj in enumerate(self._content):
+            matched = False
+            if in_preamble:
+                if self._parse_tag(lineobj):
+                    continue
+            matched = self._parse_directive(lineobj)
+            if matched:
+                if matched in self.section_identifiers:
+                    in_preamble = False
+                continue
+            self._parse_gbp_tag(linenum, lineobj)
+
+        # Update sources info (basically possible macros expanded by rpm)
+        # And, double-check that we parsed spec content correctly
+        patches = self._patches()
+        sources = self._sources()
+        for name, num, typ in self._specinfo.sources:
+            # workaround rpm parsing bug
+            if typ == 1 or typ == 9:
+                if num in sources:
+                    sources[num]['linevalue'] = name
+                else:
+                    gbp.log.err("BUG: failed to parse all 'Source' tags!")
+            elif typ == 2 or typ == 10:
+                # Patch tag without any number defined is treated by RPM as
+                # having number (2^31-1), we use number -1
+                if num >= pow(2,30):
+                    num = -1
+                if num in patches:
+                    patches[num]['linevalue'] = name
+                else:
+                    gbp.log.err("BUG: failed to parse all 'Patch' tags!")
+
+    def _delete_tag(self, tag, num):
+        """Delete a tag"""
+        key = tag.lower()
+        tagname = '%s%s' % (tag, num) if num is not None else tag
+        if key not in self._tags:
+            gbp.log.warn("Trying to delete non-existent tag '%s:'" % tag)
+            return None
+
+        sparedlines = []
+        prev = None
+        for line in self._tags[key]['lines']:
+            if line['num'] == num:
+                gbp.log.debug("Removing '%s:' tag from spec" % tagname)
+                prev = self._content.delete(line['line'])
+            else:
+                sparedlines.append(line)
+        self._tags[key]['lines'] = sparedlines
+        if not self._tags[key]['lines']:
+            self._tags.pop(key)
+        return prev
+
+    def _set_tag(self, tag, num, value, insertafter):
+        """Set a tag value"""
+        key = tag.lower()
+        tagname = '%s%s' % (tag, num) if num is not None else tag
+        value = value.strip()
+        if not value:
+            raise GbpError("Cannot set empty value to '%s:' tag" % tag)
+
+        # Check type of tag, we don't support values for 'multivalue' tags
+        try:
+            header = self._specinfo.packages[0].header
+            tagvalue = header[getattr(librpm, 'RPMTAG_%s' % tagname.upper())]
+        except AttributeError:
+            tagvalue = None
+        tagvalue = None if type(tagvalue) is list else value
+
+        # Try to guess the correct indentation from the previous or next tag
+        indent_re = re.compile(r'^([a-z]+([0-9]+)?\s*:\s*)', flags=re.I)
+        match = indent_re.match(str(insertafter))
+        if not match:
+            match = indent_re.match(str(insertafter.next))
+        indent = 12 if not match else len(match.group(1))
+        text = '%-*s%s\n' % (indent, '%s:' % tagname, value)
+        if key in self._tags:
+            self._tags[key]['value'] = tagvalue
+            for line in reversed(self._tags[key]['lines']):
+                if line['num'] == num:
+                    gbp.log.debug("Updating '%s:' tag in spec" % tagname)
+                    line['line'].set_data(text)
+                    line['linevalue'] = value
+                    return line['line']
+
+        gbp.log.debug("Adding '%s:' tag after '%s...' line in spec" %
+                      (tagname, str(insertafter)[0:20]))
+        line = self._content.insert_after(insertafter, text)
+        linerec = {'line': line, 'num': num, 'linevalue': value}
+        if key in self._tags:
+            self._tags[key]['lines'].append(linerec)
+        else:
+            self._tags[key] = {'value': tagvalue, 'lines': [linerec]}
+        return line
+
+    def set_tag(self, tag, num, value, insertafter=None):
+        """Update a tag in spec file content"""
+        key = tag.lower()
+        tagname = '%s%s' % (tag, num) if num is not None else tag
+        if key in ('patch', 'vcs'):
+            if key in self._tags:
+                insertafter = key
+            elif not insertafter in self._tags:
+                insertafter = 'name'
+            after_line = self._tags[insertafter]['lines'][-1]['line']
+            if value:
+                self._set_tag(tag, num, value, after_line)
+            elif key in self._tags:
+                self._delete_tag(tag, num)
+        else:
+            raise GbpError("Setting '%s:' tag not supported" % tagname)
+
+    def _delete_special_macro(self, name, identifier):
+        """Delete a special macro line in spec file content"""
+        if name != 'patch':
+            raise GbpError("Deleting '%s:' macro not supported" % name)
+
+        key = name.lower()
+        fullname = '%%%s%s' % (name, identifier)
+        sparedlines = []
+        prev = None
+        for line in self._special_directives[key]:
+            if line['id'] == identifier:
+                gbp.log.debug("Removing '%s' macro from spec" % fullname)
+                prev = self._content.delete(line['line'])
+            else:
+                sparedlines.append(line)
+        self._special_directives[key] = sparedlines
+        if not prev:
+            gbp.log.warn("Tried to delete non-existent macro '%s'" % fullname)
+        return prev
+
+    def _set_special_macro(self, name, identifier, args, insertafter):
+        """Update a special macro line in spec file content"""
+        key = name.lower()
+        fullname = '%%%s%s' % (name, identifier)
+        if key != 'patch':
+            raise GbpError("Setting '%s' macro not supported" % name)
+
+        updated = 0
+        text = "%%%s%d %s\n" % (name, identifier, args)
+        for line in self._special_directives[key]:
+            if line['id'] == identifier:
+                gbp.log.debug("Updating '%s' macro in spec" % fullname)
+                line['args'] = args
+                line['line'].set_data(text)
+                ret = line['line']
+                updated += 1
+        if not updated:
+            gbp.log.debug("Adding '%s' macro after '%s...' line in spec" %
+                          (fullname, str(insertafter)[0:20]))
+            ret = self._content.insert_after(insertafter, text)
+            linerec = {'line': ret, 'id': identifier, 'args': args}
+            self._special_directives[key].append(linerec)
+        return ret
+
+    def _set_section(self, name, text):
+        """Update/create a complete section in spec file."""
+        if name not in self.section_identifiers:
+            raise GbpError("Not a valid section directive: '%s'" % name)
+        # Delete section, if it exists
+        if name in self._special_directives:
+            if len(self._special_directives[name]) > 1:
+                raise GbpError("Multiple %%%s sections found, don't know "
+                               "which to update" % name)
+            line = self._special_directives[name][0]['line']
+            gbp.log.debug("Removing content of %s section" % name)
+            while line.next:
+                match = self.directive_re.match(str(line.next))
+                if match and match.group('name') in self.section_identifiers:
+                    break
+                self._content.delete(line.next)
+        else:
+            gbp.log.debug("Adding %s section to the end of spec file" % name)
+            line = self._content.append('%%%s\n' % name)
+            linerec = {'line': line, 'id': None, 'args': None}
+            self._special_directives[name] = [linerec]
+        # Add new lines
+        gbp.log.debug("Updating content of %s section" % name)
+        for linetext in text.splitlines():
+            line = self._content.insert_after(line, linetext + '\n')
+
+    def set_changelog(self, text):
+        """Update or create the %changelog section"""
+        self._set_section('changelog', text)
+
+    def get_changelog(self):
+        """Get the %changelog section"""
+        text = ''
+        if 'changelog' in self._special_directives:
+            line = self._special_directives['changelog'][0]['line']
+            while line.next:
+                line = line.next
+                match = self.directive_re.match(str(line))
+                if match and match.group('name') in self.section_identifiers:
+                    break
+                text += str(line)
+        return text
+
+    def update_patches(self, patches, commands):
+        """Update spec with new patch tags and patch macros"""
+        # Remove non-ignored patches
+        tag_prev = None
+        macro_prev = None
+        ignored = self.ignorepatches
+        # Remove 'Patch:̈́' tags
+        for tag in self._patches().values():
+            if not tag['num'] in ignored:
+                tag_prev = self._delete_tag('patch', tag['num'])
+                # Remove a preceding comment if it seems to originate from GBP
+                if re.match("^\s*#.*patch.*auto-generated",
+                            str(tag_prev), flags=re.I):
+                    tag_prev = self._content.delete(tag_prev)
+
+        # Remove '%patch:' macros
+        for macro in self._special_directives['patch']:
+            if not macro['id'] in ignored:
+                macro_prev = self._delete_special_macro('patch', macro['id'])
+                # Remove surrounding if-else
+                macro_next = macro_prev.next
+                if (str(macro_prev).startswith('%if') and
+                        str(macro_next).startswith('%endif')):
+                    self._content.delete(macro_next)
+                    macro_prev = self._content.delete(macro_prev)
+
+                # Remove a preceding comment line if it ends with '.patch' or
+                # '.diff' plus an optional compression suffix
+                if re.match("^\s*#.+(patch|diff)(\.(gz|bz2|xz|lzma))?\s*$",
+                            str(macro_prev), flags=re.I):
+                    macro_prev = self._content.delete(macro_prev)
+
+        if len(patches) == 0:
+            return
+
+        # Determine where to add Patch tag lines
+        if tag_prev:
+            gbp.log.debug("Adding 'Patch' tags in place of the removed tags")
+            tag_line = tag_prev
+        elif 'patch' in self._tags:
+            gbp.log.debug("Adding new 'Patch' tags after the last 'Patch' tag")
+            tag_line = self._tags['patch']['lines'][-1]['line']
+        elif 'source' in self._tags:
+            gbp.log.debug("Didn't find any old 'Patch' tags, adding new "
+                          "patches after the last 'Source' tag.")
+            tag_line = self._tags['source']['lines'][-1]['line']
+        else:
+            gbp.log.debug("Didn't find any old 'Patch' or 'Source' tags, "
+                          "adding new patches after the last 'Name' tag.")
+            tag_line = self._tags['name']['lines'][-1]['line']
+
+        # Determine where to add %patch macro lines
+        if 'patch-macros' in self._gbp_tags:
+            gbp.log.debug("Adding '%patch' macros after the start marker")
+            macro_line = self._gbp_tags['patch-macros'][-1]['line']
+        elif macro_prev:
+            gbp.log.debug("Adding '%patch' macros in place of the removed "
+                          "macros")
+            macro_line = macro_prev
+        elif self._special_directives['patch']:
+            gbp.log.debug("Adding new '%patch' macros after the last existing"
+                          "'%patch' macro")
+            macro_line = self._special_directives['patch'][-1]['line']
+        elif self._special_directives['setup']:
+            gbp.log.debug("Didn't find any old '%patch' macros, adding new "
+                          "patches after the last '%setup' macro")
+            macro_line = self._special_directives['setup'][-1]['line']
+        elif self._special_directives['prep']:
+            gbp.log.warn("Didn't find any old '%patch' or '%setup' macros, "
+                         "adding new patches directly after '%prep' directive")
+            macro_line = self._special_directives['prep'][-1]['line']
+        else:
+            raise GbpError("Couldn't determine where to add '%patch' macros")
+
+        startnum = sorted(ignored)[-1] + 1 if ignored else 0
+        gbp.log.debug("Starting autoupdate patch numbering from %s" % startnum)
+        # Add a comment indicating gbp generated patch tags
+        comment_text = "# Patches auto-generated by git-buildpackage:\n"
+        tag_line = self._content.insert_after(tag_line, comment_text)
+        for ind, patch in enumerate(patches):
+            cmds = commands[patch] if patch in commands else {}
+            patchnum = startnum + ind
+            tag_line = self._set_tag("Patch", patchnum, patch, tag_line)
+            # Add '%patch' macro and a preceding comment line
+            comment_text = "# %s\n" % patch
+            macro_line = self._content.insert_after(macro_line, comment_text)
+            macro_line = self._set_special_macro('patch', patchnum, '-p1',
+                                                 macro_line)
+            for cmd, args in cmds.iteritems():
+                if cmd in ('if', 'ifarch'):
+                    self._content.insert_before(macro_line, '%%%s %s\n' %
+                                                (cmd, args))
+                    macro_line = self._content.insert_after(macro_line,
+                                                            '%endif\n')
+                    # We only support one command per patch, for now
+                    break
+
+    def patchseries(self, unapplied=False, ignored=False):
+        """Return non-ignored patches of the RPM as a gbp patchseries"""
+        series = PatchSeries()
+        if 'patch' in self._tags:
+            tags = self._patches()
+            applied = []
+            for macro in self._special_directives['patch']:
+                if macro['id'] in tags:
+                    applied.append((macro['id'], macro['args']))
+            ignored = set() if ignored else set(self.ignorepatches)
+
+            # Put all patches that are applied first in the series
+            for num, args in applied:
+                if num not in ignored:
+                    opts = self._patch_macro_opts(args)
+                    strip = int(opts.strip) if opts.strip else 0
+                    filename = os.path.basename(tags[num]['linevalue'])
+                    series.append(Patch(os.path.join(self.specdir, filename),
+                                        strip=strip))
+            # Finally, append all unapplied patches to the series, if requested
+            if unapplied:
+                applied_nums = set([num for num, _args in applied])
+                unapplied = set(tags.keys()).difference(applied_nums)
+                for num in sorted(unapplied):
+                    if num not in ignored:
+                        filename = os.path.basename(tags[num]['linevalue'])
+                        series.append(Patch(os.path.join(self.specdir,
+                                                         filename), strip=0))
+        return series
+
+    def _guess_orig_prefix(self, orig):
+        """Guess prefix for the orig file"""
+        # Make initial guess about the prefix in the archive
+        filename = orig['filename']
+        name, version = RpmPkgPolicy.guess_upstream_src_version(filename)
+        if name and version:
+            prefix = "%s-%s/" % (name, version)
+        else:
+            prefix = orig['filename_base'] + "/"
+
+        # Refine our guess about the prefix
+        for macro in self._special_directives['setup']:
+            args = macro['args']
+            opts = self._setup_macro_opts(args)
+            srcnum = None
+            if opts.no_unpack_default:
+                if opts.unpack_before:
+                    srcnum = int(opts.unpack_before)
+                elif opts.unpack_after:
+                    srcnum = int(opts.unpack_after)
+            else:
+                srcnum = 0
+            if srcnum == orig['num']:
+                if opts.create_dir:
+                    prefix = ''
+                elif opts.name:
+                    try:
+                        prefix = self.macro_expand(opts.name) + '/'
+                    except MacroExpandError as err:
+                        gbp.log.warn("Couldn't determine prefix from %%setup "\
+                                     "macro (%s). Using filename base as a "  \
+                                     "fallback" % err)
+                        prefix = orig['filename_base'] + '/'
+                else:
+                    # RPM default
+                    prefix = "%s-%s/" % (self.name, self.upstreamversion)
+                break
+        return prefix
+
+    def _guess_orig_file(self):
+        """
+        Try to guess the name of the primary upstream/source archive.
+        Returns a dict with all the relevant information.
+        """
+        orig = None
+        sources = self.sources()
+        for num, filename in sorted(sources.iteritems()):
+            src = {'num': num, 'filename': os.path.basename(filename),
+                   'uri': filename}
+            src['filename_base'], src['archive_fmt'], src['compression'] = \
+                parse_archive_filename(os.path.basename(filename))
+            if (src['filename_base'].startswith(self.name) and
+                src['archive_fmt']):
+                # Take the first archive that starts with pkg name
+                orig = src
+                break
+            # otherwise we take the first archive
+            elif not orig and src['archive_fmt']:
+                orig = src
+            # else don't accept
+        if orig:
+            orig['prefix'] = self._guess_orig_prefix(orig)
+
+        return orig
+
+
+def parse_srpm(srpmfile):
+    """parse srpm by creating a SrcRpmFile object"""
+    try:
+        srcrpm = SrcRpmFile(srpmfile)
+    except IOError, err:
+        raise GbpError, "Error reading src.rpm file: %s" % err
+    except librpm.error, err:
+        raise GbpError, "RPM error while reading src.rpm: %s" % err
+
+    return srcrpm
+
+
+def guess_spec_fn(file_list, preferred_name=None):
+    """Guess spec file from a list of filenames"""
+    specs = []
+    for filepath in file_list:
+        filename = os.path.basename(filepath)
+        # Stop at the first file matching the preferred name
+        if filename == preferred_name:
+            gbp.log.debug("Found a preferred spec file %s" % filepath)
+            specs = [filepath]
+            break
+        if filename.endswith(".spec"):
+            gbp.log.debug("Found spec file %s" % filepath)
+            specs.append(filepath)
+    if len(specs) == 0:
+        raise NoSpecError("No spec file found.")
+    elif len(specs) > 1:
+        raise NoSpecError("Multiple spec files found (%s), don't know which "
+                          "to use." % ', '.join(specs))
+    return specs[0]
+
+
+def guess_spec(topdir, recursive=True, preferred_name=None):
+    """Guess a spec file"""
+    file_list = []
+    if not topdir:
+        topdir = '.'
+    for root, dirs, files in os.walk(topdir):
+        file_list.extend([os.path.join(root, fname) for fname in files])
+        if not recursive:
+            del dirs[:]
+        # Skip .git dir in any case
+        if '.git' in dirs:
+            dirs.remove('.git')
+    return SpecFile(os.path.abspath(guess_spec_fn(file_list, preferred_name)))
+
+
+def guess_spec_repo(repo, treeish, topdir='', recursive=True, preferred_name=None):
+    """
+    Try to find/parse the spec file from a given git treeish.
+    """
+    topdir = topdir.rstrip('/') + ('/') if topdir else ''
+    try:
+        file_list = [nam for (mod, typ, sha, nam) in
+                    repo.list_tree(treeish, recursive, topdir) if typ == 'blob']
+    except GitRepositoryError as err:
+        raise NoSpecError("Cannot find spec file from treeish %s, Git error: %s"
+                            % (treeish, err))
+    spec_path = guess_spec_fn(file_list, preferred_name)
+    return spec_from_repo(repo, treeish, spec_path)
+
+
+def spec_from_repo(repo, treeish, spec_path):
+    """Get and parse a spec file from a give Git treeish"""
+    try:
+        spec = SpecFile(filedata=repo.show('%s:%s' % (treeish, spec_path)))
+        spec.specdir = os.path.dirname(spec_path)
+        spec.specfile = os.path.basename(spec_path)
+        return spec
+    except GitRepositoryError as err:
+        raise NoSpecError("Git error: %s" % err)
+
+
+def string_to_int(val_str):
+    """
+    Convert string of possible unit identifier to int.
+
+    @param val_str: value to be converted
+    @type val_str: C{str}
+    @return: value as integer
+    @rtype: C{int}
+
+    >>> string_to_int("1234")
+    1234
+    >>> string_to_int("123k")
+    125952
+    >>> string_to_int("1234K")
+    1263616
+    >>> string_to_int("1M")
+    1048576
+    """
+    units = {'k': 1024,
+             'm': 1024**2,
+             'g': 1024**3,
+             't': 1024**4}
+
+    if val_str[-1].lower() in units:
+        return int(val_str[:-1]) * units[val_str[-1].lower()]
+    else:
+        return int(val_str)
+
+
+def split_version_str(version):
+    """
+    Parse full version string and split it into individual "version
+    components", i.e. upstreamversion, epoch and release
+
+    @param version: full version of a package
+    @type version: C{str}
+    @return: individual version components
+    @rtype: C{dict}
+
+    >>> split_version_str("1")
+    {'release': None, 'epoch': None, 'upstreamversion': '1'}
+    >>> split_version_str("1.2.3-5.3")
+    {'release': '5.3', 'epoch': None, 'upstreamversion': '1.2.3'}
+    >>> split_version_str("3:1.2.3")
+    {'release': None, 'epoch': '3', 'upstreamversion': '1.2.3'}
+    >>> split_version_str("3:1-0")
+    {'release': '0', 'epoch': '3', 'upstreamversion': '1'}
+    """
+    ret = {'epoch': None, 'upstreamversion': None, 'release': None}
+
+    e_vr = version.split(":", 1)
+    if len(e_vr) == 1:
+        v_r = e_vr[0].split("-", 1)
+    else:
+        ret['epoch'] = e_vr[0]
+        v_r = e_vr[1].split("-", 1)
+    ret['upstreamversion'] = v_r[0]
+    if len(v_r) > 1:
+        ret['release'] = v_r[1]
+
+    return ret
+
+def compose_version_str(evr):
+    """
+    Compose a full version string from individual "version components",
+    i.e. epoch, version and release
+
+    @param evr: dict of version components
+    @type evr: C{dict} of C{str}
+    @return: full version
+    @rtype: C{str}
+
+    >>> compose_version_str({'epoch': '', 'upstreamversion': '1.0'})
+    '1.0'
+    >>> compose_version_str({'epoch': '2', 'upstreamversion': '1.0', 'release': None})
+    '2:1.0'
+    >>> compose_version_str({'epoch': None, 'upstreamversion': '1', 'release': '0'})
+    '1-0'
+    >>> compose_version_str({'epoch': '2', 'upstreamversion': '1.0', 'release': '2.3'})
+    '2:1.0-2.3'
+    >>> compose_version_str({'epoch': '2', 'upstreamversion': '', 'release': '2.3'})
+    """
+    if 'upstreamversion' in evr and evr['upstreamversion']:
+        version = ""
+        if 'epoch' in evr and evr['epoch']:
+            version += "%s:" % evr['epoch']
+        version += evr['upstreamversion']
+        if 'release' in evr and evr['release']:
+            version += "-%s" % evr['release']
+        if version:
+            return version
+    return None
+
+
+# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
diff --git a/gbp/rpm/git.py b/gbp/rpm/git.py
new file mode 100644 (file)
index 0000000..c7cc023
--- /dev/null
@@ -0,0 +1,105 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2011 Guido Günther <agx@sigxcpu.org>
+# (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+import re
+
+from gbp.git import GitRepository, GitRepositoryError
+from gbp.pkg.pristinetar import PristineTar
+from gbp.rpm import compose_version_str
+
+class RpmGitRepository(GitRepository):
+    """A git repository that holds the source of an RPM package"""
+
+    def __init__(self, path):
+        super(RpmGitRepository, self).__init__(path)
+        self.pristine_tar = PristineTar(self)
+
+    def find_version(self, format, str_fields):
+        """
+        Check if a certain version is stored in this repo and return the SHA1
+        of the related commit. That is, an annotated tag is dereferenced to the
+        commit object it points to.
+
+        @param format: tag pattern
+        @type format: C{str}
+        @param str_fields: arguments for format string ('upstreamversion', 'release', 'vendor'...)
+        @type str_fields: C{dict} of C{str}
+        @return: sha1 of the commit the tag references to
+        """
+        try:
+            tag = self.version_to_tag(format, str_fields)
+        except KeyError:
+            return None
+        if self.has_tag(tag): # new tags are injective
+            # dereference to a commit object
+            return self.rev_parse("%s^0" % tag)
+        return None
+
+    @staticmethod
+    def version_to_tag(format, str_fields):
+        """
+        Generate a tag from a given format and a version
+
+        @param format: tag pattern
+        @type format: C{str}
+        @param str_fields: arguments for format string ('upstreamversion', 'release', 'vendor'...)
+        @type str_fields: C{dict} of C{str}
+        @return: version tag
+
+        >>> RpmGitRepository.version_to_tag("packaging/%(version)s", dict(epoch='0', upstreamversion='0~0'))
+        'packaging/0%0_0'
+        >>> RpmGitRepository.version_to_tag("%(vendor)s/v%(version)s", dict(upstreamversion='1.0', release='2', vendor="myvendor"))
+        'myvendor/v1.0-2'
+        """
+        version_tag = format % dict(str_fields,
+                                    version=compose_version_str(str_fields))
+        return RpmGitRepository._sanitize_tag(version_tag)
+
+    @staticmethod
+    def _sanitize_tag(tag):
+        """sanitize a version so git accepts it as a tag
+
+        >>> RpmGitRepository._sanitize_tag("0.0.0")
+        '0.0.0'
+        >>> RpmGitRepository._sanitize_tag("0.0~0")
+        '0.0_0'
+        >>> RpmGitRepository._sanitize_tag("0:0.0")
+        '0%0.0'
+        >>> RpmGitRepository._sanitize_tag("0%0~0")
+        '0%0_0'
+        """
+        return tag.replace('~', '_').replace(':', '%')
+
+    @property
+    def pristine_tar_branch(self):
+        """
+        The name of the pristine-tar branch, whether it already exists or
+        not.
+        """
+        return PristineTar.branch
+
+    def has_pristine_tar_branch(self):
+        """
+        Wheter the repo has a I{pristine-tar} branch.
+
+        @return: C{True} if the repo has pristine-tar commits already, C{False}
+            otherwise
+        @rtype: C{Bool}
+        """
+        return True if self.has_branch(self.pristine_tar_branch) else False
+
+# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
diff --git a/gbp/rpm/lib_rpm.py b/gbp/rpm/lib_rpm.py
new file mode 100644 (file)
index 0000000..4bad44e
--- /dev/null
@@ -0,0 +1,47 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""Wrapper module for librpm"""
+
+import tempfile
+
+import gbp.log
+from gbp.rpm.policy import RpmPkgPolicy
+
+try:
+    # Try to load special RPM lib to be used for GBP (only)
+    librpm = __import__(RpmPkgPolicy.python_rpmlib_module_name)
+except ImportError:
+    gbp.log.warn("Failed to import '%s' as rpm python module, using host's "
+                    "default rpm library instead" %
+                    RpmPkgPolicy.python_rpmlib_module_name)
+    import rpm as librpm
+
+# Module initialization
+_rpmlog = tempfile.NamedTemporaryFile(prefix='gbp_rpmlog')
+_rpmlogfd = _rpmlog.file
+librpm.setVerbosity(librpm.RPMLOG_INFO)
+librpm.setLogFile(_rpmlogfd)
+
+
+def get_librpm_log(truncate=True):
+    """Get rpmlib log output"""
+    _rpmlogfd.seek(0)
+    log = [line.strip() for line in _rpmlogfd.readlines()]
+    if truncate:
+        _rpmlogfd.truncate(0)
+    return log
+
diff --git a/gbp/rpm/linkedlist.py b/gbp/rpm/linkedlist.py
new file mode 100644 (file)
index 0000000..ca00045
--- /dev/null
@@ -0,0 +1,214 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""Simple implementation of a doubly linked list"""
+
+import collections
+
+import gbp.log
+
+
+class LinkedListNode(object):
+    """Node of the linked list"""
+
+    def __init__(self, data="", prev_node=None, next_node=None):
+        self.prev = prev_node
+        self.next = next_node
+        self._data = data
+
+    def __str__(self):
+        return str(self.data)
+
+    @property
+    def data(self):
+        """Get data stored into node"""
+        if self._data is None:
+            gbp.log.err("BUG: referencing a deleted node!")
+            return("")
+        return self._data
+
+    def set_data(self, data):
+        """
+        Set data stored into node
+
+        >>> node = LinkedListNode('foo')
+        >>> node.data
+        'foo'
+        >>> node.set_data('bar')
+        >>> node.data
+        'bar'
+        >>> node.set_data(None)
+        >>> node.data
+        ''
+        """
+        if data is None:
+            gbp.log.err("BUG: trying to store 'None', not allowed")
+            data = ""
+        self._data = data
+
+
+    def delete(self):
+        """Delete node"""
+        if self.prev:
+            self.prev.next = self.next
+        if self.next:
+            self.next.prev = self.prev
+        self._data = None
+
+
+class LinkedListIterator(collections.Iterator):
+    """Iterator for the linked list"""
+
+    def __init__(self, obj):
+        self._next = obj.first
+
+    def next(self):
+        ret = self._next
+        if ret:
+            self._next = ret.next
+        else:
+            raise StopIteration
+        return ret
+
+
+class LinkedList(collections.Iterable):
+    """Doubly linked list"""
+
+    def __init__(self):
+        self._first = None
+        self._last = None
+
+    def __iter__(self):
+        return LinkedListIterator(self)
+
+    def __len__(self):
+        for num, data in enumerate(self):
+            pass
+        return num + 1
+
+    @property
+    def first(self):
+        """Get the first node of the list"""
+        return self._first
+
+    def prepend(self, data):
+        """
+        Insert to the beginning of list
+
+        >>> list = LinkedList()
+        >>> [str(data) for data in list]
+        []
+        >>> node = list.prepend("foo")
+        >>> len(list)
+        1
+        >>> node = list.prepend("bar")
+        >>> [str(data) for data in list]
+        ['bar', 'foo']
+        """
+        if self._first is None:
+            new = self._first = self._last = LinkedListNode(data)
+        else:
+            new = self.insert_before(self._first, data)
+        return new
+
+    def append(self, data):
+        """
+        Insert to the end of list
+
+        >>> list = LinkedList()
+        >>> node = list.append('foo')
+        >>> len(list)
+        1
+        >>> node = list.append('bar')
+        >>> [str(data) for data in list]
+        ['foo', 'bar']
+        """
+        if self._last is None:
+            return self.prepend(data)
+        else:
+            return self.insert_after(self._last, data)
+
+    def insert_before(self, node, data=""):
+        """
+        Insert before a node
+
+        >>> list = LinkedList()
+        >>> node1 = list.append('foo')
+        >>> node2 = list.insert_before(node1, 'bar')
+        >>> node3 = list.insert_before(node1, 'baz')
+        >>> [str(data) for data in list]
+        ['bar', 'baz', 'foo']
+        """
+        new = LinkedListNode(data, prev_node=node.prev, next_node=node)
+        if node.prev:
+            node.prev.next = new
+        else:
+            self._first = new
+        node.prev = new
+        return new
+
+    def insert_after(self, node, data=""):
+        """
+        Insert after a node
+
+        >>> list = LinkedList()
+        >>> node1 = list.prepend('foo')
+        >>> node2 = list.insert_after(node1, 'bar')
+        >>> node3 = list.insert_after(node1, 'baz')
+        >>> [str(data) for data in list]
+        ['foo', 'baz', 'bar']
+        """
+        new = LinkedListNode(data, prev_node=node, next_node=node.next)
+        if node.next:
+            node.next.prev = new
+        else:
+            self._last = new
+        node.next = new
+        return new
+
+    def delete(self, node):
+        """
+        Delete node
+
+        >>> list = LinkedList()
+        >>> node1 = list.prepend('foo')
+        >>> node2 = list.insert_after(node1, 'bar')
+        >>> node3 = list.insert_before(node2, 'baz')
+        >>> [str(data) for data in list]
+        ['foo', 'baz', 'bar']
+        >>> str(list.delete(node3))
+        'foo'
+        >>> [str(data) for data in list]
+        ['foo', 'bar']
+        >>> print "%s" % node3
+        <BLANKLINE>
+        >>> str(list.delete(node1))
+        'bar'
+        >>> [str(data) for data in list]
+        ['bar']
+        >>> list.delete(node2)
+        >>> [str(data) for data in list]
+        []
+        """
+        ret = node.prev
+        if node is self._first:
+            ret = self._first = self._first.next
+        if node is self._last:
+            self._last = self._last.prev
+        node.delete()
+        return ret
+
+# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
diff --git a/gbp/rpm/policy.py b/gbp/rpm/policy.py
new file mode 100644 (file)
index 0000000..f8cb863
--- /dev/null
@@ -0,0 +1,72 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""Default packaging policy for RPM"""
+
+import re
+from gbp.pkg import PkgPolicy, parse_archive_filename
+
+class RpmPkgPolicy(PkgPolicy):
+    """Packaging policy for RPM"""
+
+    # Special rpmlib python module for GBP (only)
+    python_rpmlib_module_name = "rpm"
+
+    alnum = 'a-zA-Z0-9'
+    # Valid characters for RPM pkg name
+    name_whitelist_chars = '._+%{}\-'
+    # Valid characters for RPM pkg version
+    version_whitelist_chars = '._+%{}~'
+
+    # Regexp for checking the validity of package name
+    packagename_re = re.compile("^[%s][%s%s]+$" %
+                                        (alnum, alnum, name_whitelist_chars))
+    packagename_msg = ("Package names must be at least two characters long, "
+                       "start with an alphanumeric and can only contain "
+                       "alphanumerics or characters in %s" %
+                            list(name_whitelist_chars))
+
+    # Regexp for checking the validity of package (upstream) version
+    upstreamversion_re = re.compile("^[0-9][%s%s]*$" %
+                                        (alnum, version_whitelist_chars))
+    upstreamversion_msg = ("Upstream version numbers must start with a digit "
+                           "and can only containg alphanumerics or characters "
+                           "in %s" % list(version_whitelist_chars))
+
+    @classmethod
+    def is_valid_orig_archive(cls, filename):
+        """
+        Is this a valid orig source archive
+
+        @param filename: upstream source archive filename
+        @type filename: C{str}
+        @return: true if valid upstream source archive filename
+        @rtype: C{bool}
+
+        >>> RpmPkgPolicy.is_valid_orig_archive("foo/bar_baz.tar.gz")
+        True
+        >>> RpmPkgPolicy.is_valid_orig_archive("foo.bar.tar")
+        True
+        >>> RpmPkgPolicy.is_valid_orig_archive("foo.bar")
+        False
+        >>> RpmPkgPolicy.is_valid_orig_archive("foo.gz")
+        False
+        """
+        _base, arch_fmt, _compression = parse_archive_filename(filename)
+        if arch_fmt:
+            return True
+        return False
+
diff --git a/tests/20_test_rpm.py b/tests/20_test_rpm.py
new file mode 100644 (file)
index 0000000..227a1c6
--- /dev/null
@@ -0,0 +1,383 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""Test the classes under L{gbp.rpm}"""
+
+import filecmp
+import os
+import shutil
+import tempfile
+from nose.tools import assert_raises, eq_   # pylint: disable=E0611
+
+from gbp.errors import GbpError
+from gbp.rpm import (SrcRpmFile, SpecFile, parse_srpm, NoSpecError, guess_spec,
+                     guess_spec_repo, spec_from_repo)
+from gbp.git.repository import GitRepository
+
+# Disable "Method could be a function"
+#   pylint: disable=R0201
+
+
+DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data',
+                        'rpm')
+SRPM_DIR = os.path.join(DATA_DIR, 'srpms')
+SPEC_DIR = os.path.join(DATA_DIR, 'specs')
+
+
+class SpecFileTester(SpecFile):
+    """Helper class for testing"""
+
+    def protected(self, name):
+        """Get a protected member"""
+        return super(SpecFileTester, self).__getattribute__(name)
+
+
+class RpmTestBase(object):
+    """Test base class"""
+    def __init__(self):
+        self.tmpdir = None
+
+    def setup(self):
+        """Test case setup"""
+        self.tmpdir = tempfile.mkdtemp(prefix='gbp_%s_' % __name__, dir='.')
+
+    def teardown(self):
+        """Test case teardown"""
+        shutil.rmtree(self.tmpdir)
+
+class TestSpecFile(RpmTestBase):
+    """Test L{gbp.rpm.SpecFile}"""
+
+    def test_spec(self):
+        """Test parsing of a valid spec file"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test.spec')
+        spec = SpecFileTester(spec_filepath)
+
+        # Test basic properties
+        assert spec.specfile == os.path.basename(spec_filepath)
+        assert spec.specdir == os.path.dirname(spec_filepath)
+        assert spec.specpath == spec_filepath
+
+        assert spec.name == 'gbp-test'
+        assert spec.packager is None
+
+        assert spec.upstreamversion == '1.0'
+        assert spec.release == '1'
+        assert spec.epoch is None
+        assert spec.version == {'release': '1', 'upstreamversion': '1.0'}
+
+        orig = spec.orig_src
+        assert orig['filename'] == 'gbp-test-1.0.tar.bz2'
+        assert orig['uri'] == 'gbp-test-1.0.tar.bz2'
+        assert orig['filename_base'] == 'gbp-test-1.0'
+        assert orig['archive_fmt'] == 'tar'
+        assert orig['compression'] == 'bzip2'
+        assert orig['prefix'] == 'gbp-test/'
+
+    def test_spec_2(self):
+        """Test parsing of another valid spec file"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test2.spec')
+        spec = SpecFile(spec_filepath)
+
+        # Test basic properties
+        assert spec.name == 'gbp-test2'
+        assert spec.packager == 'Markus Lehtonen ' \
+                                '<markus.lehtonen@linux.intel.com>'
+
+        assert spec.epoch == '2'
+        assert spec.version == {'release': '0', 'upstreamversion': '3.0',
+                                'epoch': '2'}
+
+        orig = spec.orig_src
+        assert orig['filename'] == 'gbp-test2-3.0.tar.gz'
+        assert orig['uri'] == 'ftp://ftp.host.com/gbp-test2-3.0.tar.gz'
+        assert orig['archive_fmt'] == 'tar'
+        assert orig['compression'] == 'gzip'
+        assert orig['prefix'] == ''
+
+    def test_spec_3(self):
+        """Test parsing of yet another valid spec file"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test-native.spec')
+        spec = SpecFile(spec_filepath)
+
+        # Test basic properties
+        assert spec.name == 'gbp-test-native'
+        orig = spec.orig_src
+        assert orig['filename'] == 'gbp-test-native-1.0.zip'
+        assert orig['archive_fmt'] == 'zip'
+        assert orig['compression'] == None
+        assert orig['prefix'] == 'gbp-test-native-1.0/'
+
+    def test_spec_4(self):
+        """Test parsing of spec without orig tarball"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test-native2.spec')
+        spec = SpecFile(spec_filepath)
+
+        # Test basic properties
+        assert spec.name == 'gbp-test-native2'
+        assert spec.orig_src is None
+
+    def test_parse_raw(self):
+        """Test parsing of a valid spec file"""
+        with assert_raises(NoSpecError):
+            SpecFile(None, None)
+        with assert_raises(NoSpecError):
+            SpecFile('filename', 'filedata')
+
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test.spec')
+        with open(spec_filepath, 'r') as spec_fd:
+            spec_data = spec_fd.read()
+        spec = SpecFile(filedata=spec_data)
+
+        # Test basic properties
+        assert spec.specfile == None
+        assert spec.specdir == None
+        assert spec.name == 'gbp-test'
+
+    def test_update_spec(self):
+        """Test spec autoupdate functionality"""
+        # Create temporary spec file
+        tmp_spec = os.path.join(self.tmpdir, 'gbp-test.spec')
+        shutil.copy2(os.path.join(SPEC_DIR, 'gbp-test.spec'), tmp_spec)
+
+        reference_spec = os.path.join(SPEC_DIR, 'gbp-test-reference.spec')
+        spec = SpecFile(tmp_spec)
+        spec.update_patches(['new.patch'], {})
+        spec.write_spec_file()
+        assert filecmp.cmp(tmp_spec, reference_spec) is True
+
+        # Test adding the VCS tag and adding changelog
+        reference_spec = os.path.join(SPEC_DIR, 'gbp-test-reference2.spec')
+        spec.set_tag('VCS', None, 'myvcstag')
+        spec.set_changelog("* Wed Feb 05 2014 Name <email> 1\n- New entry\n")
+        spec.write_spec_file()
+        assert filecmp.cmp(tmp_spec, reference_spec) is True
+
+    def test_update_spec2(self):
+        """Another test for spec autoupdate functionality"""
+        tmp_spec = os.path.join(self.tmpdir, 'gbp-test2.spec')
+        shutil.copy2(os.path.join(SPEC_DIR, 'gbp-test2.spec'), tmp_spec)
+
+        reference_spec = os.path.join(SPEC_DIR, 'gbp-test2-reference2.spec')
+        spec = SpecFile(tmp_spec)
+        spec.update_patches(['1.patch', '2.patch'],
+                            {'1.patch': {'if': 'true'},
+                             '2.patch': {'ifarch': '%ix86'}})
+        spec.set_tag('VCS', None, 'myvcstag')
+        spec.write_spec_file()
+        assert filecmp.cmp(tmp_spec, reference_spec) is True
+
+        # Test updating patches again, removing the VCS tag and re-writing
+        # changelog
+        reference_spec = os.path.join(SPEC_DIR, 'gbp-test2-reference.spec')
+        spec.update_patches(['new.patch'], {'new.patch': {'if': '1'}})
+        spec.set_tag('VCS', None, '')
+        spec.set_changelog("* Wed Feb 05 2014 Name <email> 2\n- New entry\n\n")
+        spec.write_spec_file()
+        assert filecmp.cmp(tmp_spec, reference_spec) is True
+
+    def test_modifying(self):
+        """Test updating/deleting of tags and macros"""
+        tmp_spec = os.path.join(self.tmpdir, 'gbp-test.spec')
+        shutil.copy2(os.path.join(SPEC_DIR, 'gbp-test-updates.spec'), tmp_spec)
+        reference_spec = os.path.join(SPEC_DIR,
+                                      'gbp-test-updates-reference.spec')
+        spec = SpecFileTester(tmp_spec)
+
+        # Mangle tags
+        prev = spec.protected('_delete_tag')('Vendor', None)
+        spec.protected('_set_tag')('License', None, 'new license', prev)
+        spec.protected('_delete_tag')('source', 0)
+        assert spec.sources() == {}
+        spec.protected('_delete_tag')('patch', 0)
+        spec.protected('_delete_tag')('patch', -1)
+        assert spec.protected('_patches')() == {}
+        prev = spec.protected('_delete_tag')('invalidtag', None)
+
+        with assert_raises(GbpError):
+            # Check that setting empty value fails
+            spec.protected('_set_tag')('Version', None, '', prev)
+        with assert_raises(GbpError):
+            # Check that setting invalid tag with public method fails
+            spec.set_tag('invalidtag', None, 'value')
+
+        # Mangle macros
+        prev = spec.protected('_delete_special_macro')('patch', -1)
+        spec.protected('_delete_special_macro')('patch', 123)
+        spec.protected('_set_special_macro')('patch', 0, 'my new args', prev)
+        with assert_raises(GbpError):
+            spec.protected('_delete_special_macro')('invalidmacro', 0)
+        with assert_raises(GbpError):
+            spec.protected('_set_special_macro')('invalidmacro', 0, 'args',
+                           prev)
+
+        # Check resulting spec file
+        spec.write_spec_file()
+        assert filecmp.cmp(tmp_spec, reference_spec) is True
+
+    def test_modifying_err(self):
+        """Test error conditions of modification methods"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test2.spec')
+        spec = SpecFileTester(spec_filepath)
+
+        # Unknown/invalid section name
+        with assert_raises(GbpError):
+            spec.protected('_set_section')('patch', 'new content\n')
+
+        # Multiple sections with the same name
+        with assert_raises(GbpError):
+            spec.protected('_set_section')('files', '%{_sysconfdir}/foo\n')
+
+    def test_changelog(self):
+        """Test changelog methods"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test2.spec')
+        spec = SpecFile(spec_filepath)
+
+        # Read changelog
+        eq_(spec.get_changelog(),
+            "* Tue Feb 04 2014 Name <email> 1\n- My change\n\n\n")
+
+        # Set changelog and check again
+        new_text = "* Wed Feb 05 2014 Name <email> 2\n- New entry\n\n\n"
+        spec.set_changelog(new_text)
+        eq_(spec.get_changelog(), new_text)
+
+    def test_quirks(self):
+        """Test spec that is broken/has anomalities"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test-quirks.spec')
+        spec = SpecFile(spec_filepath)
+
+        # Check that we quess orig source and prefix correctly
+        assert spec.orig_src['prefix'] == 'foobar/'
+
+    def test_tags(self):
+        """Test parsing of all the different tags of spec file"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test-tags.spec')
+        spec = SpecFileTester(spec_filepath)
+
+        # Check all the tags
+        for name, val in spec.protected('_tags').iteritems():
+            rval = None
+            if name in ('version', 'release', 'epoch'):
+                rval = '0'
+            elif name in ('autoreq', 'autoprov', 'autoreqprov'):
+                rval = 'No'
+            elif name not in spec.protected('_listtags'):
+                rval = 'my_%s' % name
+            if rval:
+                assert val['value'] == rval, ("'%s:' is '%s', expecting '%s'" %
+                                              (name, val['value'], rval))
+            assert spec.ignorepatches == []
+            # Check patch numbers and patch filenames
+            patches = {}
+            for patch in spec.protected('_tags')['patch']['lines']:
+                patches[patch['num']] = patch['linevalue']
+
+            assert patches == {0: 'my_patch0', -1: 'my_patch'}
+
+    def test_patch_series(self):
+        """Test the getting the patches as a patchseries"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test-native.spec')
+        spec = SpecFileTester(spec_filepath)
+
+        assert len(spec.patchseries()) == 0
+        spec.update_patches(['1.patch', '2.patch', '3.patch'], {})
+        assert len(spec.patchseries()) == 3
+        spec.protected('_gbp_tags')['ignore-patches'].append({'args': "0"})
+        spec.update_patches(['4.patch'], {})
+        assert len(spec.patchseries()) == 1
+        assert len(spec.patchseries(ignored=True)) == 2
+        spec.protected('_delete_special_macro')('patch', 0)
+        assert len(spec.patchseries(ignored=True)) == 1
+        series = spec.patchseries(unapplied=True, ignored=True)
+        assert len(series) == 2
+        assert os.path.basename(series[-1].path) == '1.patch'
+
+    def test_patch_series_quirks(self):
+        """Patches are applied in order different from the patch numbering"""
+        spec_filepath = os.path.join(SPEC_DIR, 'gbp-test-quirks.spec')
+        spec = SpecFileTester(spec_filepath)
+
+        # Check series is returned in the order the patches are applied
+        files = [os.path.basename(patch.path) for patch in spec.patchseries()]
+        assert files == ['05.patch', '01.patch']
+        # Also ignored patches are returned in the correct order
+        files = [os.path.basename(patch.path) for patch in
+                    spec.patchseries(ignored=True)]
+        assert files == ['05.patch', '02.patch', '01.patch']
+        # Unapplied patches are added to the end of the series
+        files = [os.path.basename(patch.path) for patch in
+                    spec.patchseries(unapplied=True)]
+        assert files == ['05.patch', '01.patch', '03.patch']
+        # Return all patches (for which tag is found)
+        files = [os.path.basename(patch.path) for patch in
+                    spec.patchseries(unapplied=True, ignored=True)]
+        assert files == ['05.patch', '02.patch', '01.patch', '03.patch',
+                         '04.patch']
+
+
+class TestUtilityFunctions(RpmTestBase):
+    """Test utility functions of L{gbp.rpm}"""
+
+    def test_guess_spec(self):
+        """Test guess_spec() function"""
+        # Spec not found
+        with assert_raises(NoSpecError):
+            guess_spec(DATA_DIR, recursive=False)
+        # Multiple spec files
+        with assert_raises(NoSpecError):
+            guess_spec(DATA_DIR, recursive=True)
+        with assert_raises(NoSpecError):
+            guess_spec(SPEC_DIR, recursive=False)
+        # Spec found
+        spec = guess_spec(SPEC_DIR, recursive=False,
+                             preferred_name = 'gbp-test2.spec')
+        assert spec.specfile == 'gbp-test2.spec'
+        assert spec.specdir == SPEC_DIR
+
+    def test_guess_spec_repo(self):
+        """Test guess_spec_repo() and spec_from_repo() functions"""
+        # Create dummy repository with some commits
+        repo = GitRepository.create(self.tmpdir)
+        with open(os.path.join(repo.path, 'foo.txt'), 'w') as fobj:
+            fobj.write('bar\n')
+        repo.add_files('foo.txt')
+        repo.commit_all('Add dummy file')
+        os.mkdir(os.path.join(repo.path, 'packaging'))
+        shutil.copy(os.path.join(SPEC_DIR, 'gbp-test.spec'),
+                    os.path.join(repo.path, 'packaging'))
+        repo.add_files('packaging/gbp-test.spec')
+        repo.commit_all('Add spec file')
+
+        # Spec not found
+        with assert_raises(NoSpecError):
+            guess_spec_repo(repo, 'HEAD~1', recursive=True)
+        with assert_raises(NoSpecError):
+            guess_spec_repo(repo, 'HEAD', recursive=False)
+        # Spec found
+        spec = guess_spec_repo(repo, 'HEAD', 'packaging', recursive=False)
+        spec = guess_spec_repo(repo, 'HEAD', recursive=True)
+        assert spec.specfile == 'gbp-test.spec'
+        assert spec.specdir == 'packaging'
+        assert spec.specpath == 'packaging/gbp-test.spec'
+
+        # Test spec_from_repo()
+        with assert_raises(NoSpecError):
+            spec_from_repo(repo, 'HEAD~1', 'packaging/gbp-test.spec')
+        spec = spec_from_repo(repo, 'HEAD', 'packaging/gbp-test.spec')
+        assert spec.specfile == 'gbp-test.spec'
+
+# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
diff --git a/tests/data/rpm/rpmbuild/SOURCES/bar.tar.gz b/tests/data/rpm/rpmbuild/SOURCES/bar.tar.gz
new file mode 100644 (file)
index 0000000..f5dae80
Binary files /dev/null and b/tests/data/rpm/rpmbuild/SOURCES/bar.tar.gz differ
diff --git a/tests/data/rpm/rpmbuild/SOURCES/foo.txt b/tests/data/rpm/rpmbuild/SOURCES/foo.txt
new file mode 100644 (file)
index 0000000..25ed442
--- /dev/null
@@ -0,0 +1,3 @@
+FOO:
+
+file for testing rpm support of git-buildpackage.
diff --git a/tests/data/rpm/rpmbuild/SOURCES/gbp-test-1.0.tar.bz2 b/tests/data/rpm/rpmbuild/SOURCES/gbp-test-1.0.tar.bz2
new file mode 100644 (file)
index 0000000..7d0759f
Binary files /dev/null and b/tests/data/rpm/rpmbuild/SOURCES/gbp-test-1.0.tar.bz2 differ
diff --git a/tests/data/rpm/rpmbuild/SOURCES/gbp-test-native-1.0.zip b/tests/data/rpm/rpmbuild/SOURCES/gbp-test-native-1.0.zip
new file mode 100644 (file)
index 0000000..22a273d
Binary files /dev/null and b/tests/data/rpm/rpmbuild/SOURCES/gbp-test-native-1.0.zip differ
diff --git a/tests/data/rpm/rpmbuild/SOURCES/gbp-test2-3.0.tar.gz b/tests/data/rpm/rpmbuild/SOURCES/gbp-test2-3.0.tar.gz
new file mode 100644 (file)
index 0000000..7b3eaf3
Binary files /dev/null and b/tests/data/rpm/rpmbuild/SOURCES/gbp-test2-3.0.tar.gz differ
diff --git a/tests/data/rpm/rpmbuild/SOURCES/my.patch b/tests/data/rpm/rpmbuild/SOURCES/my.patch
new file mode 100644 (file)
index 0000000..50870df
--- /dev/null
@@ -0,0 +1,9 @@
+diff --git a/dummy.sh b/dummy.sh
+index 8c33db6..6f04268 100755
+--- dummy.sh
++++ dummy.sh
+@@ -1,3 +1,3 @@
+ #!/bin/sh
+-echo "Hello world"
++echo "Hello GBP"
diff --git a/tests/data/rpm/rpmbuild/SOURCES/my2.patch b/tests/data/rpm/rpmbuild/SOURCES/my2.patch
new file mode 100644 (file)
index 0000000..ad5ca2d
--- /dev/null
@@ -0,0 +1,7 @@
+diff --git a/mydir/myfile.txt b/mydir/myfile.txt
+new file mode 100644
+index 0000000..2cdad29
+--- /dev/null
++++ b/mydir/myfile.txt
+@@ -0,0 +1 @@
++Dummy
diff --git a/tests/data/rpm/rpmbuild/SOURCES/my3.patch b/tests/data/rpm/rpmbuild/SOURCES/my3.patch
new file mode 100644 (file)
index 0000000..9fee859
--- /dev/null
@@ -0,0 +1,7 @@
+diff --git a/README b/README
+index a1311cb..a59f1b9 100644
+--- a/README
++++ b/README
+@@ -1 +1 @@
+-Just for testing git-buildpackage.
++Just for testing GBP.
diff --git a/tests/data/rpm/rpmbuild/SPECS/gbp-test-native.spec b/tests/data/rpm/rpmbuild/SPECS/gbp-test-native.spec
new file mode 100644 (file)
index 0000000..38b07e4
--- /dev/null
@@ -0,0 +1,34 @@
+Name:       gbp-test-native
+Summary:    Test package for git-buildpackage
+Version:    1.0
+Release:    1
+Group:      Development/Libraries
+License:    GPLv2
+Source1:    %{name}-%{version}.zip
+BuildRequires:  unzip
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+Mimics a "native" package
+
+
+%prep
+unzip %{SOURCE1}
+%setup -T -D
+
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
diff --git a/tests/data/rpm/rpmbuild/SPECS/gbp-test-native2.spec b/tests/data/rpm/rpmbuild/SPECS/gbp-test-native2.spec
new file mode 100644 (file)
index 0000000..34fd33d
--- /dev/null
@@ -0,0 +1,35 @@
+Name:       gbp-test-native2
+Summary:    Test package for git-buildpackage
+Version:    2.0
+Release:    0
+Group:      Development/Libraries
+License:    GPLv2
+Source:     foo.txt
+BuildRequires:  unzip
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+Mimics a "native" package that doesn't have any source tarball.
+
+
+%prep
+# Just create build dir
+%setup -T -c
+cp %{SOURCE0} .
+
+
+%build
+# Nothing to do
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
diff --git a/tests/data/rpm/rpmbuild/SPECS/gbp-test.spec b/tests/data/rpm/rpmbuild/SPECS/gbp-test.spec
new file mode 100644 (file)
index 0000000..c46a734
--- /dev/null
@@ -0,0 +1,42 @@
+Name:       gbp-test
+Summary:    Test package for git-buildpackage
+Version:    1.0
+Release:    1
+Group:      Development/Libraries
+License:    GPLv2
+Source:     %{name}-%{version}.tar.bz2
+Source1:    foo.txt
+Source20:   bar.tar.gz
+# Gbp-Ignore-Patches: 0
+Patch0:     my.patch
+Patch10:    my2.patch
+Patch20:    my3.patch
+
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+
+
+%prep
+%setup -n %{name} -a 20
+
+%patch0
+%patch10 -p1
+
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+install %{SOURCE0} %{buildroot}/%{_datadir}/%{name}
+
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
diff --git a/tests/data/rpm/rpmbuild/SPECS/gbp-test2.spec b/tests/data/rpm/rpmbuild/SPECS/gbp-test2.spec
new file mode 100644 (file)
index 0000000..8a92725
--- /dev/null
@@ -0,0 +1,60 @@
+Name:       gbp-test2
+Summary:    Test package 2 for git-buildpackage
+Epoch:      2
+Version:    3.0
+Release:    0
+Group:      Development/Libraries
+License:    GPLv2
+Source10:   ftp://ftp.host.com/%{name}-%{version}.tar.gz
+Source:     foo.txt
+Source20:   bar.tar.gz
+# Gbp-Ignore-Patches: -1
+Patch:      my.patch
+Patch10:    my2.patch
+Patch20:    my3.patch
+Packager:   Markus Lehtonen <markus.lehtonen@linux.intel.com>
+VCS:        myoldvcstag
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+
+%package empty
+Summary:    Empty subpackage
+
+%description empty
+Empty subpackage for the %{name} test package.
+
+
+%prep
+%setup -T -n %{name}-%{version} -c -a 10
+
+%patch
+%patch -P 10 -p1
+
+echo "Do things"
+
+# Gbp-Patch-Macros
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+install %{SOURCE0} %{buildroot}/%{_datadir}/%{name}
+
+
+%changelog
+* Tue Feb 04 2014 Name <email> 1
+- My change
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
+
+%files empty
+%defattr(-,root,root,-)
diff --git a/tests/data/rpm/specs/gbp-test-native.spec b/tests/data/rpm/specs/gbp-test-native.spec
new file mode 120000 (symlink)
index 0000000..60de36f
--- /dev/null
@@ -0,0 +1 @@
+../rpmbuild/SPECS/gbp-test-native.spec
\ No newline at end of file
diff --git a/tests/data/rpm/specs/gbp-test-native2.spec b/tests/data/rpm/specs/gbp-test-native2.spec
new file mode 120000 (symlink)
index 0000000..ad13ad6
--- /dev/null
@@ -0,0 +1 @@
+../rpmbuild/SPECS/gbp-test-native2.spec
\ No newline at end of file
diff --git a/tests/data/rpm/specs/gbp-test-quirks.spec b/tests/data/rpm/specs/gbp-test-quirks.spec
new file mode 100644 (file)
index 0000000..bb56b00
--- /dev/null
@@ -0,0 +1,30 @@
+#
+# Spec for testing some quirks of spec parsing
+#
+
+Name:           pkg_name
+Summary:        Spec for testing some quirks of spec parsing
+Version:        0.1
+Release:        1.2
+License:        GPLv2
+Source1:        foobar.tar.gz
+# Gbp-Ignore-Patches: 2 4 888
+Patch1:         01.patch
+Patch2:         02.patch
+Patch3:         03.patch
+Patch4:         04.patch
+Patch5:         05.patch
+
+%description
+Spec for testing some quirks of spec parsing. No intended for building an RPM.
+
+%prep
+# We don't have Source0 so rpmbuild would fail, but gbp shouldn't crash
+%setup -q
+
+# Patches are applied out-of-order wrt. numbering
+%patch5
+%patch2
+%patch1
+# Patch 999 does not exist, rpmbuild would fail but GBP should not
+%patch999
diff --git a/tests/data/rpm/specs/gbp-test-reference.spec b/tests/data/rpm/specs/gbp-test-reference.spec
new file mode 100644 (file)
index 0000000..050d139
--- /dev/null
@@ -0,0 +1,43 @@
+Name:       gbp-test
+Summary:    Test package for git-buildpackage
+Version:    1.0
+Release:    1
+Group:      Development/Libraries
+License:    GPLv2
+Source:     %{name}-%{version}.tar.bz2
+Source1:    foo.txt
+Source20:   bar.tar.gz
+# Gbp-Ignore-Patches: 0
+Patch0:     my.patch
+# Patches auto-generated by git-buildpackage:
+Patch1:     new.patch
+
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+
+
+%prep
+%setup -n %{name} -a 20
+
+%patch0
+# new.patch
+%patch1 -p1
+
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+install %{SOURCE0} %{buildroot}/%{_datadir}/%{name}
+
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
diff --git a/tests/data/rpm/specs/gbp-test-reference2.spec b/tests/data/rpm/specs/gbp-test-reference2.spec
new file mode 100644 (file)
index 0000000..0fbe026
--- /dev/null
@@ -0,0 +1,47 @@
+Name:       gbp-test
+VCS:        myvcstag
+Summary:    Test package for git-buildpackage
+Version:    1.0
+Release:    1
+Group:      Development/Libraries
+License:    GPLv2
+Source:     %{name}-%{version}.tar.bz2
+Source1:    foo.txt
+Source20:   bar.tar.gz
+# Gbp-Ignore-Patches: 0
+Patch0:     my.patch
+# Patches auto-generated by git-buildpackage:
+Patch1:     new.patch
+
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+
+
+%prep
+%setup -n %{name} -a 20
+
+%patch0
+# new.patch
+%patch1 -p1
+
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+install %{SOURCE0} %{buildroot}/%{_datadir}/%{name}
+
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
+%changelog
+* Wed Feb 05 2014 Name <email> 1
+- New entry
diff --git a/tests/data/rpm/specs/gbp-test-tags.spec b/tests/data/rpm/specs/gbp-test-tags.spec
new file mode 100644 (file)
index 0000000..ee4c2b9
--- /dev/null
@@ -0,0 +1,74 @@
+#
+# Spec file for testing all RPM tags (that we know of
+#
+
+%define suse_release %(test -e /etc/SuSE-release && head -n1 /etc/SuSE-release | cut -d ' ' -f2 | cut --output-delimiter=0 -d. -f1,2 || echo 0)
+%if "%{suse_release}" >= "1201"
+%define test_weak_dep_tags 1
+%endif
+
+%define test_arch_os_tags %(test -n "$GBP_SKIP_ARCH_OS_TAGS" && echo 0 || echo 1)
+
+%define source_fn_base source
+%define patch_fn_base patch
+
+# Gbp-Undefined-Tag: foobar
+
+# Test that we accept different cases
+NAME:           my_name
+version:        0
+ReLeasE:        0
+
+# Rest of the tags
+Epoch:          0
+Summary:        my_summary
+License:        my_license
+Distribution:   my_distribution
+Vendor:         my_vendor
+Group:          my_group
+Packager:       my_packager
+Url:            my_url
+Vcs:            my_vcs
+Source:         my_source
+Patch:          my_%patch_fn_base
+Patch0:         my_%{patch_fn_base}0
+Nosource:       0
+Nopatch:        0
+#Icon:           my_icon
+BuildRoot:      my_buildroot
+Provides:       my_provides
+Requires:       my_requires
+Conflicts:      my_conflicts
+Obsoletes:      my_obsoletes
+BuildConflicts: my_buildconflicts
+BuildRequires:  my_buildrequires
+AutoReqProv:    No
+AutoReq:        No
+AutoProv:       No
+DistTag:        my_disttag
+BugUrl:         my_bugurl
+Collections:    my_collections
+
+%if 0%{?test_weak_dep_tags}
+Recommends:     my_recommends
+Suggests:       my_suggests
+Supplements:    my_supplements
+Enhances:       my_enhances
+BuildRecommends:my_buildrecommends
+BuildSuggests:  my_buildsuggests
+BuildSupplements:my_buildsupplements
+BuildEnhances:  my_buildenhances
+%endif
+
+# These should be filtered out by GBP
+%if "%{test_arch_os_tags}" != "0"
+BuildArch:      my_buildarch
+ExcludeArch:    my_excludearch
+ExclusiveArch:  my_exclusivearch
+ExcludeOs:      my_excludeos
+ExclusiveOs:    my_exclusiveos
+%endif
+
+%description
+Package for testing GBP.
+
diff --git a/tests/data/rpm/specs/gbp-test-updates-reference.spec b/tests/data/rpm/specs/gbp-test-updates-reference.spec
new file mode 100644 (file)
index 0000000..ff56f58
--- /dev/null
@@ -0,0 +1,44 @@
+#
+# Spec file for testing deleting/adding/updating tags and macros
+#
+
+# Gbp-Undefined-Tag: foobar
+
+# Test that we accept different cases
+Name:           my_name
+Version:        0
+Release:        1
+Summary:        my_summary
+License:        new license
+Distribution:   my_distribution
+Group:          my_group
+Packager:       my_packager
+Url:            my_url
+Vcs:            my_vcs
+Nosource:       0
+Nopatch:        0
+BuildRoot:      my_buildroot
+Provides:       my_provides
+Requires:       my_requires
+Conflicts:      my_conflicts
+Obsoletes:      my_obsoletes
+BuildConflicts: my_buildconflicts
+BuildRequires:  my_buildrequires
+AutoReqProv:    No
+AutoReq:        No
+AutoProv:       No
+DistTag:        my_disttag
+BugUrl:         my_bugurl
+Collections:    my_collections
+
+%description
+Package for testing GBP.
+
+%prep
+%setup -n my_prefix
+
+%patch0 my new args
+
+%build
+
+%install
diff --git a/tests/data/rpm/specs/gbp-test-updates.spec b/tests/data/rpm/specs/gbp-test-updates.spec
new file mode 100644 (file)
index 0000000..dc8ffbf
--- /dev/null
@@ -0,0 +1,49 @@
+#
+# Spec file for testing deleting/adding/updating tags and macros
+#
+
+# Gbp-Undefined-Tag: foobar
+
+# Test that we accept different cases
+Name:           my_name
+Version:        0
+Release:        1
+Summary:        my_summary
+License:        my_license
+Distribution:   my_distribution
+Vendor:         my_vendor
+Group:          my_group
+Packager:       my_packager
+Url:            my_url
+Vcs:            my_vcs
+Source:         my_source
+Patch:          my_%patch_fn_base
+Patch0:         my_%{patch_fn_base}0
+Nosource:       0
+Nopatch:        0
+BuildRoot:      my_buildroot
+Provides:       my_provides
+Requires:       my_requires
+Conflicts:      my_conflicts
+Obsoletes:      my_obsoletes
+BuildConflicts: my_buildconflicts
+BuildRequires:  my_buildrequires
+AutoReqProv:    No
+AutoReq:        No
+AutoProv:       No
+DistTag:        my_disttag
+BugUrl:         my_bugurl
+Collections:    my_collections
+
+%description
+Package for testing GBP.
+
+%prep
+%setup -n my_prefix
+
+%patch -b my_patch
+%patch -P0 -b my_patch0
+
+%build
+
+%install
diff --git a/tests/data/rpm/specs/gbp-test.spec b/tests/data/rpm/specs/gbp-test.spec
new file mode 120000 (symlink)
index 0000000..30ae284
--- /dev/null
@@ -0,0 +1 @@
+../rpmbuild/SPECS/gbp-test.spec
\ No newline at end of file
diff --git a/tests/data/rpm/specs/gbp-test2-reference.spec b/tests/data/rpm/specs/gbp-test2-reference.spec
new file mode 100644 (file)
index 0000000..1882131
--- /dev/null
@@ -0,0 +1,61 @@
+Name:       gbp-test2
+Summary:    Test package 2 for git-buildpackage
+Epoch:      2
+Version:    3.0
+Release:    0
+Group:      Development/Libraries
+License:    GPLv2
+Source10:   ftp://ftp.host.com/%{name}-%{version}.tar.gz
+Source:     foo.txt
+Source20:   bar.tar.gz
+# Gbp-Ignore-Patches: -1
+Patch:      my.patch
+# Patches auto-generated by git-buildpackage:
+Patch0:     new.patch
+Packager:   Markus Lehtonen <markus.lehtonen@linux.intel.com>
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+
+%package empty
+Summary:    Empty subpackage
+
+%description empty
+Empty subpackage for the %{name} test package.
+
+
+%prep
+%setup -T -n %{name}-%{version} -c -a 10
+
+%patch
+
+echo "Do things"
+
+# Gbp-Patch-Macros
+# new.patch
+%if 1
+%patch0 -p1
+%endif
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+install %{SOURCE0} %{buildroot}/%{_datadir}/%{name}
+
+
+%changelog
+* Wed Feb 05 2014 Name <email> 2
+- New entry
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
+
+%files empty
+%defattr(-,root,root,-)
diff --git a/tests/data/rpm/specs/gbp-test2-reference2.spec b/tests/data/rpm/specs/gbp-test2-reference2.spec
new file mode 100644 (file)
index 0000000..d41f450
--- /dev/null
@@ -0,0 +1,68 @@
+Name:       gbp-test2
+Summary:    Test package 2 for git-buildpackage
+Epoch:      2
+Version:    3.0
+Release:    0
+Group:      Development/Libraries
+License:    GPLv2
+Source10:   ftp://ftp.host.com/%{name}-%{version}.tar.gz
+Source:     foo.txt
+Source20:   bar.tar.gz
+# Gbp-Ignore-Patches: -1
+Patch:      my.patch
+# Patches auto-generated by git-buildpackage:
+Patch0:     1.patch
+Patch1:     2.patch
+Packager:   Markus Lehtonen <markus.lehtonen@linux.intel.com>
+VCS:        myvcstag
+
+%description
+Package for testing the RPM functionality of git-buildpackage.
+
+%package empty
+Summary:    Empty subpackage
+
+%description empty
+Empty subpackage for the %{name} test package.
+
+
+%prep
+%setup -T -n %{name}-%{version} -c -a 10
+
+%patch
+
+echo "Do things"
+
+# Gbp-Patch-Macros
+# 1.patch
+%if true
+%patch0 -p1
+%endif
+# 2.patch
+%ifarch %ix86
+%patch1 -p1
+%endif
+
+%build
+make
+
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}/%{_datadir}/%{name}
+cp -R * %{buildroot}/%{_datadir}/%{name}
+install %{SOURCE0} %{buildroot}/%{_datadir}/%{name}
+
+
+%changelog
+* Tue Feb 04 2014 Name <email> 1
+- My change
+
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/%{name}
+%{_datadir}/%{name}
+
+%files empty
+%defattr(-,root,root,-)
diff --git a/tests/data/rpm/specs/gbp-test2.spec b/tests/data/rpm/specs/gbp-test2.spec
new file mode 120000 (symlink)
index 0000000..af4080c
--- /dev/null
@@ -0,0 +1 @@
+../rpmbuild/SPECS/gbp-test2.spec
\ No newline at end of file