chiark / gitweb /
Escape: Add missing r in regexp literals ('...' => r'...') [7]
[git-buildpackage.git] / gbp / rpm / __init__.py
1 # vim: set fileencoding=utf-8 :
2 #
3 # (C) 2006,2007 Guido Günther <agx@sigxcpu.org>
4 # (C) 2012 Intel Corporation <markus.lehtonen@linux.intel.com>
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program; if not, please see
17 #    <http://www.gnu.org/licenses/>
18 """provides some rpm source package related helpers"""
19
20 import os
21 import re
22 import tempfile
23 from optparse import OptionParser
24 from collections import defaultdict
25
26 import gbp.command_wrappers as gbpc
27 from gbp.errors import GbpError
28 from gbp.git import GitRepositoryError
29 from gbp.patch_series import (PatchSeries, Patch)
30 import gbp.log
31 from gbp.pkg import (UpstreamSource, Archive)
32 from gbp.rpm.policy import RpmPkgPolicy
33 from gbp.rpm.linkedlist import LinkedList
34 from gbp.rpm.lib_rpm import librpm, get_librpm_log
35
36
37 def _decode(s):
38     if s is not None:
39         return s.decode()
40
41
42 class NoSpecError(Exception):
43     """Spec file parsing error"""
44     pass
45
46
47 class MacroExpandError(Exception):
48     """Macro expansion in spec file failed"""
49     pass
50
51
52 class RpmUpstreamSource(UpstreamSource):
53     """Upstream source class for RPM packages"""
54     def __init__(self, name, unpacked=None, **kwargs):
55         super(RpmUpstreamSource, self).__init__(name,
56                                                 unpacked,
57                                                 RpmPkgPolicy,
58                                                 **kwargs)
59
60
61 class SrcRpmFile(object):
62     """Keeps all needed data read from a source rpm"""
63     def __init__(self, srpmfile):
64         # Do not required signed packages to be able to import
65         ts_vsflags = 0
66         for flag in ['RPMVSF_NOMD5HEADER', 'RPMVSF_NORSAHEADER',
67                      'RPMVSF_NOSHA1HEADER', 'RPMVSF_NODSAHEADER',
68                      'RPMVSF_NOMD5', 'RPMVSF_NORSA', 'RPMVSF_NOSHA1',
69                      'RPMVSF_NODSA']:
70             try:
71                 # Ignore flags not present in different librpm versions
72                 ts_vsflags |= getattr(librpm, flag)
73             except AttributeError:
74                 pass
75         with open(srpmfile) as srpmfp:
76             self.rpmhdr = librpm.ts(vsflags=ts_vsflags).hdrFromFdno(srpmfp.fileno())
77         self.srpmfile = os.path.abspath(srpmfile)
78
79     @property
80     def version(self):
81         """Get the (downstream) version of the RPM package"""
82         version = dict(upstreamversion=self.rpmhdr[librpm.RPMTAG_VERSION].decode(),
83                        release=self.rpmhdr[librpm.RPMTAG_RELEASE].decode())
84         if self.rpmhdr[librpm.RPMTAG_EPOCH] is not None:
85             version['epoch'] = str(self.rpmhdr[librpm.RPMTAG_EPOCH])
86         return version
87
88     @property
89     def name(self):
90         """Get the name of the RPM package"""
91         return self.rpmhdr[librpm.RPMTAG_NAME].decode()
92
93     @property
94     def upstreamversion(self):
95         """Get the upstream version of the RPM package"""
96         return self.rpmhdr[librpm.RPMTAG_VERSION].decode()
97
98     @property
99     def packager(self):
100         """Get the packager of the RPM package"""
101         return _decode(self.rpmhdr[librpm.RPMTAG_PACKAGER])
102
103     def unpack(self, dest_dir):
104         """
105         Unpack the source rpm to tmpdir.
106         Leave the cleanup to the caller in case of an error.
107         """
108         c = gbpc.RunAtCommand('rpm2cpio',
109                               [self.srpmfile, '|', 'cpio', '-id'],
110                               shell=True, capture_stderr=True)
111         c.run_error = "'%s' failed: {stderr_or_reason}" % (" ".join([c.cmd] + c.args))
112         c(dir=dest_dir)
113
114
115 class SpecFile(object):
116     """Class for parsing/modifying spec files"""
117     tag_re = re.compile(r'^(?P<name>[a-z]+)(?P<num>[0-9]+)?\s*:\s*'
118                         r'(?P<value>\S(.*\S)?)\s*$', flags=re.I)
119     directive_re = re.compile(r'^%(?P<name>[a-z]+)(?P<num>[0-9]+)?'
120                               r'(\s+(?P<args>.*))?$', flags=re.I)
121     gbptag_re = re.compile(r'^\s*#\s*gbp-(?P<name>[a-z-]+)'
122                            r'(\s*:\s*(?P<args>\S.*))?$', flags=re.I)
123     # Here "sections" stand for all scripts, scriptlets and other directives,
124     # but not macros
125     section_identifiers = ('package', 'description', 'prep', 'build', 'install',
126                            'clean', 'check', 'pre', 'preun', 'post', 'postun', 'verifyscript',
127                            'files', 'changelog', 'triggerin', 'triggerpostin', 'triggerun',
128                            'triggerpostun')
129
130     def __init__(self, filename=None, filedata=None):
131
132         self._content = LinkedList()
133
134         # Check args: only filename or filedata can be given, not both
135         if filename is None and filedata is None:
136             raise NoSpecError("No filename or raw data given for parsing!")
137         elif filename and filedata:
138             raise NoSpecError("Both filename and raw data given, don't know "
139                               "which one to parse!")
140         elif filename:
141             # Load spec file into our special data structure
142             self.specfile = os.path.basename(filename)
143             self.specdir = os.path.dirname(os.path.abspath(filename))
144             try:
145                 with open(filename) as spec_file:
146                     for line in spec_file.readlines():
147                         self._content.append(line)
148             except IOError as err:
149                 raise NoSpecError("Unable to read spec file: %s" % err)
150         else:
151             self.specfile = None
152             self.specdir = None
153             for line in filedata.splitlines():
154                 self._content.append(line + '\n')
155
156         # Use rpm-python to parse the spec file content
157         self._filtertags = ("excludearch", "excludeos", "exclusivearch",
158                             "exclusiveos", "buildarch")
159         self._listtags = self._filtertags + ('source', 'patch',
160                                              'requires', 'conflicts', 'recommends',
161                                              'suggests', 'supplements', 'enhances',
162                                              'provides', 'obsoletes', 'buildrequires',
163                                              'buildconflicts', 'buildrecommends',
164                                              'buildsuggests', 'buildsupplements',
165                                              'buildenhances', 'collections',
166                                              'nosource', 'nopatch')
167         self._specinfo = self._parse_filtered_spec(self._filtertags)
168
169         # Other initializations
170         source_header = self._specinfo.packages[0].header
171         self.name = source_header[librpm.RPMTAG_NAME].decode()
172         self.upstreamversion = source_header[librpm.RPMTAG_VERSION].decode()
173         self.release = source_header[librpm.RPMTAG_RELEASE].decode()
174         # rpm-python returns epoch as 'long', convert that to string
175         self.epoch = str(source_header[librpm.RPMTAG_EPOCH]) \
176             if source_header[librpm.RPMTAG_EPOCH] is not None else None
177         self.packager = _decode(source_header[librpm.RPMTAG_PACKAGER])
178         self._tags = {}
179         self._special_directives = defaultdict(list)
180         self._gbp_tags = defaultdict(list)
181
182         # Parse extra info from spec file
183         self._parse_content()
184
185         # Find 'Packager' tag. Needed to circumvent a bug in python-rpm where
186         # spec.sourceHeader[librpm.RPMTAG_PACKAGER] is not reset when a new spec
187         # file is parsed
188         if 'packager' not in self._tags:
189             self.packager = None
190
191         self.orig_src = self._guess_orig_file()
192
193     def _parse_filtered_spec(self, skip_tags):
194         """Parse a filtered spec file in rpm-python"""
195         skip_tags = [tag.lower() for tag in skip_tags]
196         with tempfile.NamedTemporaryFile(prefix='gbp', mode='w+') as filtered:
197             filtered.writelines(str(line) for line in self._content
198                                 if str(line).split(":")[0].strip().lower() not in skip_tags)
199             filtered.flush()
200             try:
201                 # Parse two times to circumvent a rpm-python problem where
202                 # macros are not expanded if used before their definition
203                 librpm.spec(filtered.name)
204                 return librpm.spec(filtered.name)
205             except ValueError as err:
206                 rpmlog = get_librpm_log()
207                 gbp.log.debug("librpm log:\n        %s" %
208                               "\n        ".join(rpmlog))
209                 raise GbpError("RPM error while parsing %s: %s (%s)" %
210                                (self.specfile, err, rpmlog[-1]))
211
212     @property
213     def version(self):
214         """Get the (downstream) version"""
215         version = dict(upstreamversion=self.upstreamversion,
216                        release=self.release)
217         if self.epoch is not None:
218             version['epoch'] = self.epoch
219         return version
220
221     @property
222     def specpath(self):
223         """Get the dir/filename"""
224         return os.path.join(self.specdir, self.specfile)
225
226     @property
227     def ignorepatches(self):
228         """Get numbers of ignored patches as a sorted list"""
229         if 'ignore-patches' in self._gbp_tags:
230             data = self._gbp_tags['ignore-patches'][-1]['args'].split()
231             return sorted([int(num) for num in data])
232         return []
233
234     def _patches(self):
235         """Get all patch tags as a dict"""
236         if 'patch' not in self._tags:
237             return {}
238         return {patch['num']: patch for patch in self._tags['patch']['lines']}
239
240     def _sources(self):
241         """Get all source tags as a dict"""
242         if 'source' not in self._tags:
243             return {}
244         return {src['num']: src for src in self._tags['source']['lines']}
245
246     def sources(self):
247         """Get all source tags as a dict"""
248         return {src['num']: src['linevalue']
249                 for src in self._sources().values()}
250
251     def _macro_replace(self, matchobj):
252         macro_dict = {'name': self.name,
253                       'version': self.upstreamversion,
254                       'release': self.release}
255
256         if matchobj.group(2) in macro_dict:
257             return macro_dict[matchobj.group(2)]
258         raise MacroExpandError("Unknown macro '%s'" % matchobj.group(0))
259
260     def macro_expand(self, text):
261         """
262         Expand the rpm macros (that gbp knows of) in the given text.
263
264         @param text: text to check for macros
265         @type text: C{str}
266         @return: text with macros expanded
267         @rtype: C{str}
268         """
269         # regexp to match '%{macro}' and '%macro'
270         macro_re = re.compile(r'%({)?(?P<macro_name>[a-z_][a-z0-9_]*)(?(1)})', flags=re.I)
271         return macro_re.sub(self._macro_replace, text)
272
273     def write_spec_file(self):
274         """
275         Write, possibly updated, spec to disk
276         """
277         with open(os.path.join(self.specdir, self.specfile), 'w') as spec_file:
278             for line in self._content:
279                 spec_file.write(str(line))
280
281     def _parse_tag(self, lineobj):
282         """Parse tag line"""
283
284         line = str(lineobj)
285
286         matchobj = self.tag_re.match(line)
287         if not matchobj:
288             return False
289
290         tagname = matchobj.group('name').lower()
291         tagnum = int(matchobj.group('num')) if matchobj.group('num') else None
292         # 'Source:' tags
293         if tagname == 'source':
294             tagnum = 0 if tagnum is None else tagnum
295         # 'Patch:' tags
296         elif tagname == 'patch':
297             tagnum = -1 if tagnum is None else tagnum
298
299         # Record all tag locations
300         try:
301             header = self._specinfo.packages[0].header
302             tagvalue = header[getattr(librpm, 'RPMTAG_%s' % tagname.upper())]
303         except AttributeError:
304             tagvalue = None
305         # We don't support "multivalue" tags like "Provides:" or "SourceX:"
306         # Rpm python doesn't support many of these, thus the explicit list
307         if isinstance(tagvalue, int):
308             tagvalue = str(tagvalue)
309         elif type(tagvalue) is list or tagname in self._listtags:
310             tagvalue = None
311         elif not tagvalue:
312             # Rpm python doesn't give the following, for reason or another
313             if tagname not in ('buildroot', 'autoprov', 'autoreq',
314                                'autoreqprov') + self._filtertags:
315                 gbp.log.warn("BUG: '%s:' tag not found by rpm" % tagname)
316             tagvalue = matchobj.group('value')
317         linerecord = {'line': lineobj,
318                       'num': tagnum,
319                       'linevalue': matchobj.group('value')}
320         if tagname in self._tags:
321             self._tags[tagname]['value'] = tagvalue
322             self._tags[tagname]['lines'].append(linerecord)
323         else:
324             if tagvalue and not isinstance(tagvalue, str):
325                 tagvalue = tagvalue.decode()
326             self._tags[tagname] = {'value': tagvalue, 'lines': [linerecord]}
327
328         return tagname
329
330     @staticmethod
331     def _patch_macro_opts(args):
332         """Parse arguments of the '%patch' macro"""
333
334         patchparser = OptionParser(
335             prog="%s internal patch macro opts parser" % __name__,
336             usage="%prog for " + args)
337         patchparser.add_option("-p", dest="strip")
338         patchparser.add_option("-s", dest="silence")
339         patchparser.add_option("-P", dest="patchnum")
340         patchparser.add_option("-b", dest="backup")
341         patchparser.add_option("-E", dest="removeempty")
342         patchparser.add_option("-F", dest="fuzz")
343         arglist = args.split()
344         return patchparser.parse_args(arglist)[0]
345
346     @staticmethod
347     def _setup_macro_opts(args):
348         """Parse arguments of the '%setup' macro"""
349
350         setupparser = OptionParser(
351             prog="%s internal setup macro opts parser" % __name__,
352             usage="%prog for " + args)
353         setupparser.add_option("-n", dest="name")
354         setupparser.add_option("-c", dest="create_dir", action="store_true")
355         setupparser.add_option("-D", dest="no_delete_dir", action="store_true")
356         setupparser.add_option("-T", dest="no_unpack_default",
357                                action="store_true")
358         setupparser.add_option("-b", dest="unpack_before")
359         setupparser.add_option("-a", dest="unpack_after")
360         setupparser.add_option("-q", dest="quiet", action="store_true")
361         arglist = args.split()
362         return setupparser.parse_args(arglist)[0]
363
364     def _parse_directive(self, lineobj):
365         """Parse special directive/scriptlet/macro lines"""
366
367         line = str(lineobj)
368         matchobj = self.directive_re.match(line)
369         if not matchobj:
370             return None
371
372         directivename = matchobj.group('name')
373         # '%patch' macros
374         directiveid = None
375         if directivename == 'patch':
376             opts = self._patch_macro_opts(matchobj.group('args'))
377             if matchobj.group('num'):
378                 directiveid = int(matchobj.group('num'))
379             elif opts.patchnum:
380                 directiveid = int(opts.patchnum)
381             else:
382                 directiveid = -1
383
384         # Record special directive/scriptlet/macro locations
385         if directivename in self.section_identifiers + ('setup', 'patch',
386                                                         'autosetup'):
387             linerecord = {'line': lineobj,
388                           'id': directiveid,
389                           'args': matchobj.group('args')}
390             self._special_directives[directivename].append(linerecord)
391         return directivename
392
393     def _parse_gbp_tag(self, linenum, lineobj):
394         """Parse special git-buildpackage tags"""
395
396         line = str(lineobj)
397         matchobj = self.gbptag_re.match(line)
398         if matchobj:
399             gbptagname = matchobj.group('name').lower()
400             if gbptagname not in ('ignore-patches', 'patch-macros'):
401                 gbp.log.info("Found unrecognized Gbp tag on line %s: '%s'" %
402                              (linenum, line))
403             if matchobj.group('args'):
404                 args = matchobj.group('args').strip()
405             else:
406                 args = None
407             record = {'line': lineobj, 'args': args}
408             self._gbp_tags[gbptagname].append(record)
409             return gbptagname
410
411         return None
412
413     def _parse_content(self):
414         """
415         Go through spec file content line-by-line and (re-)parse info from it
416         """
417         in_preamble = True
418         for linenum, lineobj in enumerate(self._content):
419             matched = False
420             if in_preamble:
421                 if self._parse_tag(lineobj):
422                     continue
423             matched = self._parse_directive(lineobj)
424             if matched:
425                 if matched in self.section_identifiers:
426                     in_preamble = False
427                 continue
428             self._parse_gbp_tag(linenum, lineobj)
429
430         # Update sources info (basically possible macros expanded by rpm)
431         # And, double-check that we parsed spec content correctly
432         patches = self._patches()
433         sources = self._sources()
434         for name, num, typ in self._specinfo.sources:
435             # workaround rpm parsing bug
436             if typ == 1 or typ == 9:
437                 if num in sources:
438                     sources[num]['linevalue'] = name
439                 else:
440                     gbp.log.err("BUG: failed to parse all 'Source' tags!")
441             elif typ == 2 or typ == 10:
442                 # Patch tag without any number defined is treated by RPM as
443                 # having number (2^31-1), we use number -1
444                 if num >= pow(2, 30):
445                     num = -1
446                 if num in patches:
447                     patches[num]['linevalue'] = name
448                 else:
449                     gbp.log.err("BUG: failed to parse all 'Patch' tags!")
450
451     def _delete_tag(self, tag, num):
452         """Delete a tag"""
453         key = tag.lower()
454         tagname = '%s%s' % (tag, num) if num is not None else tag
455         if key not in self._tags:
456             gbp.log.warn("Trying to delete non-existent tag '%s:'" % tag)
457             return None
458
459         sparedlines = []
460         prev = None
461         for line in self._tags[key]['lines']:
462             if line['num'] == num:
463                 gbp.log.debug("Removing '%s:' tag from spec" % tagname)
464                 prev = self._content.delete(line['line'])
465             else:
466                 sparedlines.append(line)
467         self._tags[key]['lines'] = sparedlines
468         if not self._tags[key]['lines']:
469             self._tags.pop(key)
470         return prev
471
472     def _set_tag(self, tag, num, value, insertafter):
473         """Set a tag value"""
474         key = tag.lower()
475         tagname = '%s%s' % (tag, num) if num is not None else tag
476         value = value.strip()
477         if not value:
478             raise GbpError("Cannot set empty value to '%s:' tag" % tag)
479
480         # Check type of tag, we don't support values for 'multivalue' tags
481         try:
482             header = self._specinfo.packages[0].header
483             tagvalue = header[getattr(librpm, 'RPMTAG_%s' % tagname.upper())]
484         except AttributeError:
485             tagvalue = None
486         tagvalue = None if type(tagvalue) is list else value
487
488         # Try to guess the correct indentation from the previous or next tag
489         indent_re = re.compile(r'^([a-z]+([0-9]+)?\s*:\s*)', flags=re.I)
490         match = indent_re.match(str(insertafter))
491         if not match:
492             match = indent_re.match(str(insertafter.next))
493         indent = 12 if not match else len(match.group(1))
494         text = '%-*s%s\n' % (indent, '%s:' % tagname, value)
495         if key in self._tags:
496             self._tags[key]['value'] = tagvalue
497             for line in reversed(self._tags[key]['lines']):
498                 if line['num'] == num:
499                     gbp.log.debug("Updating '%s:' tag in spec" % tagname)
500                     line['line'].set_data(text)
501                     line['linevalue'] = value
502                     return line['line']
503
504         gbp.log.debug("Adding '%s:' tag after '%s...' line in spec" %
505                       (tagname, str(insertafter)[0:20]))
506         line = self._content.insert_after(insertafter, text)
507         linerec = {'line': line, 'num': num, 'linevalue': value}
508         if key in self._tags:
509             self._tags[key]['lines'].append(linerec)
510         else:
511             self._tags[key] = {'value': tagvalue, 'lines': [linerec]}
512         return line
513
514     def set_tag(self, tag, num, value, insertafter=None):
515         """Update a tag in spec file content"""
516         key = tag.lower()
517         tagname = '%s%s' % (tag, num) if num is not None else tag
518         if key in ('patch', 'vcs'):
519             if key in self._tags:
520                 insertafter = key
521             elif insertafter not in self._tags:
522                 insertafter = 'name'
523             after_line = self._tags[insertafter]['lines'][-1]['line']
524             if value:
525                 self._set_tag(tag, num, value, after_line)
526             elif key in self._tags:
527                 self._delete_tag(tag, num)
528         else:
529             raise GbpError("Setting '%s:' tag not supported" % tagname)
530
531     def _delete_special_macro(self, name, identifier):
532         """Delete a special macro line in spec file content"""
533         if name != 'patch':
534             raise GbpError("Deleting '%s:' macro not supported" % name)
535
536         key = name.lower()
537         fullname = '%%%s%s' % (name, identifier)
538         sparedlines = []
539         prev = None
540         for line in self._special_directives[key]:
541             if line['id'] == identifier:
542                 gbp.log.debug("Removing '%s' macro from spec" % fullname)
543                 prev = self._content.delete(line['line'])
544             else:
545                 sparedlines.append(line)
546         self._special_directives[key] = sparedlines
547         if not prev:
548             gbp.log.warn("Tried to delete non-existent macro '%s'" % fullname)
549         return prev
550
551     def _set_special_macro(self, name, identifier, args, insertafter):
552         """Update a special macro line in spec file content"""
553         key = name.lower()
554         fullname = '%%%s%s' % (name, identifier)
555         if key != 'patch':
556             raise GbpError("Setting '%s' macro not supported" % name)
557
558         updated = 0
559         text = "%%%s%d %s\n" % (name, identifier, args)
560         for line in self._special_directives[key]:
561             if line['id'] == identifier:
562                 gbp.log.debug("Updating '%s' macro in spec" % fullname)
563                 line['args'] = args
564                 line['line'].set_data(text)
565                 ret = line['line']
566                 updated += 1
567         if not updated:
568             gbp.log.debug("Adding '%s' macro after '%s...' line in spec" %
569                           (fullname, str(insertafter)[0:20]))
570             ret = self._content.insert_after(insertafter, text)
571             linerec = {'line': ret, 'id': identifier, 'args': args}
572             self._special_directives[key].append(linerec)
573         return ret
574
575     def _set_section(self, name, text):
576         """Update/create a complete section in spec file."""
577         if name not in self.section_identifiers:
578             raise GbpError("Not a valid section directive: '%s'" % name)
579         # Delete section, if it exists
580         if name in self._special_directives:
581             if len(self._special_directives[name]) > 1:
582                 raise GbpError("Multiple %%%s sections found, don't know "
583                                "which to update" % name)
584             line = self._special_directives[name][0]['line']
585             gbp.log.debug("Removing content of %s section" % name)
586             while line.next:
587                 match = self.directive_re.match(str(line.next))
588                 if match and match.group('name') in self.section_identifiers:
589                     break
590                 self._content.delete(line.next)
591         else:
592             gbp.log.debug("Adding %s section to the end of spec file" % name)
593             line = self._content.append('%%%s\n' % name)
594             linerec = {'line': line, 'id': None, 'args': None}
595             self._special_directives[name] = [linerec]
596         # Add new lines
597         gbp.log.debug("Updating content of %s section" % name)
598         for linetext in text.splitlines():
599             line = self._content.insert_after(line, linetext + '\n')
600
601     def set_changelog(self, text):
602         """Update or create the %changelog section"""
603         self._set_section('changelog', text)
604
605     def get_changelog(self):
606         """Get the %changelog section"""
607         text = ''
608         if 'changelog' in self._special_directives:
609             line = self._special_directives['changelog'][0]['line']
610             while line.next:
611                 line = line.next
612                 match = self.directive_re.match(str(line))
613                 if match and match.group('name') in self.section_identifiers:
614                     break
615                 text += str(line)
616         return text
617
618     def update_patches(self, patches, commands):
619         """Update spec with new patch tags and patch macros"""
620         # Remove non-ignored patches
621         tag_prev = None
622         macro_prev = None
623         ignored = self.ignorepatches
624         # Remove 'Patch:̈́' tags
625         for tag in self._patches().values():
626             if not tag['num'] in ignored:
627                 tag_prev = self._delete_tag('patch', tag['num'])
628                 # Remove a preceding comment if it seems to originate from GBP
629                 if re.match("^\s*#.*patch.*auto-generated",
630                             str(tag_prev), flags=re.I):
631                     tag_prev = self._content.delete(tag_prev)
632
633         # Remove '%patch:' macros
634         for macro in self._special_directives['patch']:
635             if not macro['id'] in ignored:
636                 macro_prev = self._delete_special_macro('patch', macro['id'])
637                 # Remove surrounding if-else
638                 macro_next = macro_prev.next
639                 if (str(macro_prev).startswith('%if') and
640                         str(macro_next).startswith('%endif')):
641                     self._content.delete(macro_next)
642                     macro_prev = self._content.delete(macro_prev)
643
644                 # Remove a preceding comment line if it ends with '.patch' or
645                 # '.diff' plus an optional compression suffix
646                 if re.match("^\s*#.+(patch|diff)(\.(gz|bz2|xz|lzma))?\s*$",
647                             str(macro_prev), flags=re.I):
648                     macro_prev = self._content.delete(macro_prev)
649
650         if len(patches) == 0:
651             return
652
653         # Determine where to add Patch tag lines
654         if tag_prev:
655             gbp.log.debug("Adding 'Patch' tags in place of the removed tags")
656             tag_line = tag_prev
657         elif 'patch' in self._tags:
658             gbp.log.debug("Adding new 'Patch' tags after the last 'Patch' tag")
659             tag_line = self._tags['patch']['lines'][-1]['line']
660         elif 'source' in self._tags:
661             gbp.log.debug("Didn't find any old 'Patch' tags, adding new "
662                           "patches after the last 'Source' tag.")
663             tag_line = self._tags['source']['lines'][-1]['line']
664         else:
665             gbp.log.debug("Didn't find any old 'Patch' or 'Source' tags, "
666                           "adding new patches after the last 'Name' tag.")
667             tag_line = self._tags['name']['lines'][-1]['line']
668
669         # Determine where to add %patch macro lines
670         if self._special_directives['autosetup']:
671             gbp.log.debug("Found '%autosetup, skip adding %patch macros")
672             macro_line = None
673         elif 'patch-macros' in self._gbp_tags:
674             gbp.log.debug("Adding '%patch' macros after the start marker")
675             macro_line = self._gbp_tags['patch-macros'][-1]['line']
676         elif macro_prev:
677             gbp.log.debug("Adding '%patch' macros in place of the removed "
678                           "macros")
679             macro_line = macro_prev
680         elif self._special_directives['patch']:
681             gbp.log.debug("Adding new '%patch' macros after the last existing"
682                           "'%patch' macro")
683             macro_line = self._special_directives['patch'][-1]['line']
684         elif self._special_directives['setup']:
685             gbp.log.debug("Didn't find any old '%patch' macros, adding new "
686                           "patches after the last '%setup' macro")
687             macro_line = self._special_directives['setup'][-1]['line']
688         elif self._special_directives['prep']:
689             gbp.log.warn("Didn't find any old '%patch' or '%setup' macros, "
690                          "adding new patches directly after '%prep' directive")
691             macro_line = self._special_directives['prep'][-1]['line']
692         else:
693             raise GbpError("Couldn't determine where to add '%patch' macros")
694
695         startnum = sorted(ignored)[-1] + 1 if ignored else 0
696         gbp.log.debug("Starting autoupdate patch numbering from %s" % startnum)
697         # Add a comment indicating gbp generated patch tags
698         comment_text = "# Patches auto-generated by git-buildpackage:\n"
699         tag_line = self._content.insert_after(tag_line, comment_text)
700         for ind, patch in enumerate(patches):
701             cmds = commands[patch] if patch in commands else {}
702             patchnum = startnum + ind
703             tag_line = self._set_tag("Patch", patchnum, patch, tag_line)
704
705             # Add '%patch' macro and a preceding comment line
706             if macro_line is not None:
707                 comment_text = "# %s\n" % patch
708                 macro_line = self._content.insert_after(macro_line, comment_text)
709                 macro_line = self._set_special_macro('patch', patchnum, '-p1',
710                                                      macro_line)
711                 for cmd, args in cmds.items():
712                     if cmd in ('if', 'ifarch'):
713                         self._content.insert_before(macro_line, '%%%s %s\n' %
714                                                     (cmd, args))
715                         macro_line = self._content.insert_after(macro_line,
716                                                                 '%endif\n')
717                         # We only support one command per patch, for now
718                         break
719
720     def patchseries(self, unapplied=False, ignored=False):
721         """Return non-ignored patches of the RPM as a gbp patchseries"""
722         series = PatchSeries()
723
724         ignored = set() if ignored else set(self.ignorepatches)
725         tags = dict([(k, v) for k, v in self._patches().items() if k not in ignored])
726
727         if self._special_directives['autosetup']:
728             # Return all patchses if %autosetup is used
729             for num in sorted(tags):
730                 filename = os.path.basename(tags[num]['linevalue'])
731                 series.append(Patch(os.path.join(self.specdir, filename)))
732         else:
733             applied = []
734             for macro in self._special_directives['patch']:
735                 if macro['id'] in tags:
736                     applied.append((macro['id'], macro['args']))
737
738             # Put all patches that are applied first in the series
739             for num, args in applied:
740                 opts = self._patch_macro_opts(args)
741                 strip = int(opts.strip) if opts.strip else 0
742                 filename = os.path.basename(tags[num]['linevalue'])
743                 series.append(Patch(os.path.join(self.specdir, filename),
744                                     strip=strip))
745             # Finally, append all unapplied patches to the series, if requested
746             if unapplied:
747                 applied_nums = set([num for num, _args in applied])
748                 unapplied = set(tags.keys()).difference(applied_nums)
749                 for num in sorted(unapplied):
750                     filename = os.path.basename(tags[num]['linevalue'])
751                     series.append(Patch(os.path.join(self.specdir, filename),
752                                         strip=0))
753         return series
754
755     def _guess_orig_prefix(self, orig):
756         """Guess prefix for the orig file"""
757         # Make initial guess about the prefix in the archive
758         filename = orig['filename']
759         name, version = RpmPkgPolicy.guess_upstream_src_version(filename)
760         if name and version:
761             prefix = "%s-%s/" % (name, version)
762         else:
763             prefix = orig['filename_base'] + "/"
764
765         # Refine our guess about the prefix
766         for macro in self._special_directives['setup']:
767             args = macro['args']
768             opts = self._setup_macro_opts(args)
769             srcnum = None
770             if opts.no_unpack_default:
771                 if opts.unpack_before:
772                     srcnum = int(opts.unpack_before)
773                 elif opts.unpack_after:
774                     srcnum = int(opts.unpack_after)
775             else:
776                 srcnum = 0
777             if srcnum == orig['num']:
778                 if opts.create_dir:
779                     prefix = ''
780                 elif opts.name:
781                     try:
782                         prefix = self.macro_expand(opts.name) + '/'
783                     except MacroExpandError as err:
784                         gbp.log.warn("Couldn't determine prefix from %%setup "
785                                      "macro (%s). Using filename base as a "
786                                      "fallback" % err)
787                         prefix = orig['filename_base'] + '/'
788                 else:
789                     # RPM default
790                     prefix = "%s-%s/" % (self.name, self.upstreamversion)
791                 break
792         return prefix
793
794     def _guess_orig_file(self):
795         """
796         Try to guess the name of the primary upstream/source archive.
797         Returns a dict with all the relevant information.
798         """
799         orig = None
800         sources = self.sources()
801         for num, filename in sorted(sources.items()):
802             src = {'num': num, 'filename': os.path.basename(filename),
803                    'uri': filename}
804             src['filename_base'], src['archive_fmt'], src['compression'] = \
805                 Archive.parse_filename(os.path.basename(filename))
806             if (src['filename_base'].startswith(self.name) and
807                     src['archive_fmt']):
808                 # Take the first archive that starts with pkg name
809                 orig = src
810                 break
811             # otherwise we take the first archive
812             elif not orig and src['archive_fmt']:
813                 orig = src
814             # else don't accept
815         if orig:
816             orig['prefix'] = self._guess_orig_prefix(orig)
817
818         return orig
819
820
821 def parse_srpm(srpmfile):
822     """parse srpm by creating a SrcRpmFile object"""
823     try:
824         srcrpm = SrcRpmFile(srpmfile)
825     except IOError as err:
826         raise GbpError("Error reading src.rpm file: %s" % err)
827     except librpm.error as err:
828         raise GbpError("RPM error while reading src.rpm: %s" % err)
829
830     return srcrpm
831
832
833 def guess_spec_fn(file_list, preferred_name=None):
834     """Guess spec file from a list of filenames"""
835     specs = []
836     for filepath in file_list:
837         filename = os.path.basename(filepath)
838         # Stop at the first file matching the preferred name
839         if filename == preferred_name:
840             gbp.log.debug("Found a preferred spec file %s" % filepath)
841             specs = [filepath]
842             break
843         if filename.endswith(".spec"):
844             gbp.log.debug("Found spec file %s" % filepath)
845             specs.append(filepath)
846     if len(specs) == 0:
847         raise NoSpecError("No spec file found.")
848     elif len(specs) > 1:
849         raise NoSpecError("Multiple spec files found (%s), don't know which "
850                           "to use." % ', '.join(specs))
851     return specs[0]
852
853
854 def guess_spec(topdir, recursive=True, preferred_name=None):
855     """Guess a spec file"""
856     file_list = []
857     if not topdir:
858         topdir = '.'
859     for root, dirs, files in os.walk(topdir):
860         file_list.extend([os.path.join(root, fname) for fname in files])
861         if not recursive:
862             del dirs[:]
863         # Skip .git dir in any case
864         if '.git' in dirs:
865             dirs.remove('.git')
866     return SpecFile(os.path.abspath(guess_spec_fn(file_list, preferred_name)))
867
868
869 def guess_spec_repo(repo, treeish, topdir='', recursive=True, preferred_name=None):
870     """
871     Try to find/parse the spec file from a given git treeish.
872     """
873     topdir = topdir.rstrip('/') + ('/') if topdir else ''
874     try:
875         file_list = [nam.decode() for (mod, typ, sha, nam) in
876                      repo.list_tree(treeish, recursive, topdir) if typ == 'blob']
877     except GitRepositoryError as err:
878         raise NoSpecError("Cannot find spec file from treeish %s, Git error: %s"
879                           % (treeish, err))
880     spec_path = guess_spec_fn(file_list, preferred_name)
881     return spec_from_repo(repo, treeish, spec_path)
882
883
884 def spec_from_repo(repo, treeish, spec_path):
885     """Get and parse a spec file from a give Git treeish"""
886     try:
887         spec = SpecFile(filedata=repo.show('%s:%s' % (treeish, spec_path)).decode())
888         spec.specdir = os.path.dirname(spec_path)
889         spec.specfile = os.path.basename(spec_path)
890         return spec
891     except GitRepositoryError as err:
892         raise NoSpecError("Git error: %s" % err)
893
894
895 def string_to_int(val_str):
896     """
897     Convert string of possible unit identifier to int.
898
899     @param val_str: value to be converted
900     @type val_str: C{str}
901     @return: value as integer
902     @rtype: C{int}
903
904     >>> string_to_int("1234")
905     1234
906     >>> string_to_int("123k")
907     125952
908     >>> string_to_int("1234K")
909     1263616
910     >>> string_to_int("1M")
911     1048576
912     """
913     units = {'k': 1024,
914              'm': 1024**2,
915              'g': 1024**3,
916              't': 1024**4}
917
918     if val_str[-1].lower() in units:
919         return int(val_str[:-1]) * units[val_str[-1].lower()]
920     else:
921         return int(val_str)
922
923
924 def split_version_str(version):
925     """
926     Parse full version string and split it into individual "version
927     components", i.e. upstreamversion, epoch and release
928
929     @param version: full version of a package
930     @type version: C{str}
931     @return: individual version components
932     @rtype: C{dict}
933
934     >>> sorted(split_version_str("1").items())
935     [('epoch', None), ('release', None), ('upstreamversion', '1')]
936     >>> sorted(split_version_str("1.2.3-5.3").items())
937     [('epoch', None), ('release', '5.3'), ('upstreamversion', '1.2.3')]
938     >>> sorted(split_version_str("3:1.2.3").items())
939     [('epoch', '3'), ('release', None), ('upstreamversion', '1.2.3')]
940     >>> sorted(split_version_str("3:1-0").items())
941     [('epoch', '3'), ('release', '0'), ('upstreamversion', '1')]
942     """
943     ret = {'epoch': None, 'upstreamversion': None, 'release': None}
944
945     e_vr = version.split(":", 1)
946     if len(e_vr) == 1:
947         v_r = e_vr[0].split("-", 1)
948     else:
949         ret['epoch'] = e_vr[0]
950         v_r = e_vr[1].split("-", 1)
951     ret['upstreamversion'] = v_r[0]
952     if len(v_r) > 1:
953         ret['release'] = v_r[1]
954
955     return ret
956
957
958 def compose_version_str(evr):
959     """
960     Compose a full version string from individual "version components",
961     i.e. epoch, version and release
962
963     @param evr: dict of version components
964     @type evr: C{dict} of C{str}
965     @return: full version
966     @rtype: C{str}
967
968     >>> compose_version_str({'epoch': '', 'upstreamversion': '1.0'})
969     '1.0'
970     >>> compose_version_str({'epoch': '2', 'upstreamversion': '1.0', 'release': None})
971     '2:1.0'
972     >>> compose_version_str({'epoch': None, 'upstreamversion': '1', 'release': '0'})
973     '1-0'
974     >>> compose_version_str({'epoch': '2', 'upstreamversion': '1.0', 'release': '2.3'})
975     '2:1.0-2.3'
976     >>> compose_version_str({'epoch': '2', 'upstreamversion': '', 'release': '2.3'})
977     """
978     if 'upstreamversion' in evr and evr['upstreamversion']:
979         version = ""
980         if 'epoch' in evr and evr['epoch']:
981             version += "%s:" % evr['epoch']
982         version += evr['upstreamversion']
983         if 'release' in evr and evr['release']:
984             version += "-%s" % evr['release']
985         if version:
986             return version
987     return None
988
989
990 def filter_version(evr, *keys):
991     """
992     Remove entry from the version dict
993
994     @param evr: dict of version components
995     @type evr: C{dict} of C{str}
996     @param keys: keys to remove
997     @type keys: C{str}s
998     @return: new version dict
999     @rtype: C{dict} of C{str}
1000
1001     >>> sorted(list(filter_version({'epoch': 'foo', 'upstreamversion': 'bar', 'vendor': 'baz'}, 'vendor').keys()))
1002     ['epoch', 'upstreamversion']
1003     >>> list(filter_version({'epoch': 'foo', 'upstreamversion': 'bar', 'revision': 'baz'}, 'epoch', 'revision').keys())
1004     ['upstreamversion']
1005     """
1006     return {k: evr[k] for k in evr if k not in keys}
1007
1008
1009 # vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: