1 # -*- coding: utf-8 -*-
2 #############################################################################
5 # Author : Frederic Lepied
6 # Created on : Tue Sep 28 07:18:06 1999
7 # Version : $Id: Pkg.py 1892 2011-11-23 20:21:05Z scop $
8 # Purpose : provide an API to handle a rpm package either by accessing
9 # the rpm file or by accessing the files contained inside.
10 #############################################################################
24 # TODO: magic.MAGIC_COMPRESS when PkgFile gets decompress support.
25 _magic = magic.open(magic.MAGIC_NONE)
33 # Python 2/3 compatibility/convenience wrapper for printing to stderr with
34 # less concerns of UnicodeErrors than plain sys.stderr.write.
35 if sys.version_info[0] > 2:
36 # Blows up with Python < 3 without the exec() hack
37 exec('def warn(s): print (s, file=sys.stderr)')
39 def warn(s): print >> sys.stderr, s
44 # 64: RPMSENSE_PREREQ is 0 with recent rpm versions, we want 64 here in order
45 # to do the right thing with packages built with older rpm versions
46 PREREQ_FLAG = (rpm.RPMSENSE_PREREQ or 64) | \
47 rpm.RPMSENSE_SCRIPT_PRE | \
48 rpm.RPMSENSE_SCRIPT_POST | \
49 rpm.RPMSENSE_SCRIPT_PREUN | \
50 rpm.RPMSENSE_SCRIPT_POSTUN
52 var_regex = re.compile('^(.*)\${?(\w+)}?(.*)$')
54 def shell_var_value(var, script):
55 assign_regex = re.compile('\\b' + re.escape(var) + '\s*=\s*(.+)\s*(#.*)*$',
57 res = assign_regex.search(script)
59 res2 = var_regex.search(res.group(1))
61 if res2.group(2) == var: # infinite loop
63 return substitute_shell_vars(res.group(1), script)
67 def substitute_shell_vars(val, script):
68 res = var_regex.search(val)
70 value = shell_var_value(res.group(2), script)
73 return res.group(1) + value + \
74 substitute_shell_vars(res.group(3), script)
78 def getstatusoutput(cmd, stdoutonly = False):
79 '''A version of commands.getstatusoutput() which can take cmd as a
80 sequence, thus making it potentially more secure.'''
82 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
83 stdout=subprocess.PIPE, close_fds=True)
85 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.STDOUT, close_fds=True)
89 text = proc.stdout.read()
93 if text.endswith('\n'):
97 bz2_regex = re.compile('\.t?bz2?$')
98 xz_regex = re.compile('\.(t[xl]z|xz|lzma)$')
101 """Get a 'cat' command that handles possibly compressed files."""
103 if bz2_regex.search(fname):
105 elif xz_regex.search(fname):
110 (sts, text) = getstatusoutput(catcmd(fname).split() + [fname])
111 return not sts and is_utf8_str(text)
123 elif isinstance(string, unicode):
126 x = unicode(string, 'ascii')
129 encodings = ['utf-8', 'iso-8859-1', 'iso-8859-15', 'iso-8859-2']
130 for enc in encodings:
132 x = unicode(string, enc)
136 if x.encode(enc) == string:
137 return x.encode('utf-8')
141 newstring = newstring + '?'
143 newstring = newstring + char
147 fobj = open(path, "r")
149 return fobj.readlines()
154 tmpfd, tmpname = tempfile.mkstemp(prefix = 'rpmlint.')
155 tmpfile = os.fdopen(tmpfd, 'w')
156 return tmpfile, tmpname
158 slash_regex = re.compile('/+')
159 slashdot_regex = re.compile('/(\.(/|$))+')
160 slashend_regex = re.compile('([^/])/+$')
162 def safe_normpath(path):
163 """Like os.path.normpath but normalizes less aggressively thus being
164 potentially safer for paths containing symlinks."""
165 ret = slash_regex.sub('/', path)
166 ret = slashdot_regex.sub('\\2', ret)
167 ret = slashend_regex.sub('\\1', ret)
170 def get_default_valid_rpmgroups(filename = None):
171 """Get default rpm groups from filename, or try to look them up from
172 the rpm package (if installed) if no filename is given"""
176 p = InstalledPkg('rpm')
180 groupsfiles = [x for x in p.files() if x.endswith('/GROUPS')]
182 filename = groupsfiles[0]
183 if filename and os.path.exists(filename):
184 fobj = open(filename)
186 groups = fobj.read().strip().splitlines()
189 if 'Development/Debug' not in groups:
190 groups.append('Development/Debug')
191 if 'Unspecified' not in groups: # auto-added by rpm >= 4.6.0
192 groups.append('Unspecified')
196 # from yum 3.2.27, rpmUtils.miscutils, with rpmlint modifications
197 def compareEVR((e1, v1, r1), (e2, v2, r2)):
198 # return 1: a is newer than b
199 # 0: a and b are the same version
200 # -1: b is newer than a
201 # rpmlint mod: don't stringify None epochs to 'None' strings
210 rc = rpm.labelCompare((e1, v1, r1), (e2, v2, r2))
213 # from yum 3.2.27, rpmUtils.miscutils, with rpmlint modifications
214 def rangeCompare(reqtuple, provtuple):
215 """returns true if provtuple satisfies reqtuple"""
216 (reqn, reqf, (reqe, reqv, reqr)) = reqtuple
217 (n, f, (e, v, r)) = provtuple
221 # unversioned satisfies everything
222 if not f or not reqf:
225 # and you thought we were done having fun
226 # if the requested release is left out then we have
227 # to remove release from the package prco to make sure the match
228 # is a success - ie: if the request is EQ foo 1:3.0.0 and we have
229 # foo 1:3.0.0-15 then we have to drop the 15 so we can match
232 # rpmlint mod: don't mess with provided Epoch, doing so breaks e.g.
233 # "Requires: foo < 1.0" should not be satisfied by "Provides: foo = 1:0.5"
236 if reqv is None: # just for the record if ver is None then we're going to segfault
239 # if we just require foo-version, then foo-version-* will match
243 rc = compareEVR((e, v, r), (reqe, reqv, reqr))
245 # does not match unless
247 if reqf in ['GT', 'GE', 4, 12]:
249 if reqf in ['EQ', 8]:
250 if f in ['LE', 10, 'LT', 2]:
252 if reqf in ['LE', 'LT', 'EQ', 10, 2, 8]:
253 if f in ['LE', 'LT', 10, 2]:
257 if reqf in ['GT', 4]:
258 if f in ['GT', 'GE', 4, 12]:
260 if reqf in ['GE', 12]:
261 if f in ['GT', 'GE', 'EQ', 'LE', 4, 12, 8, 10]:
263 if reqf in ['EQ', 8]:
264 if f in ['EQ', 'GE', 'LE', 8, 12, 10]:
266 if reqf in ['LE', 10]:
267 if f in ['EQ', 'LE', 'LT', 'GE', 8, 10, 2, 12]:
269 if reqf in ['LT', 2]:
270 if f in ['LE', 'LT', 10, 2]:
273 if reqf in ['GT', 'GE', 'EQ', 4, 12, 8]:
274 if f in ['GT', 'GE', 4, 12]:
276 if reqf in ['LE', 'LT', 10, 2]:
279 # if reqf in ['GT', 'GE', 4, 12]:
282 # if reqf in ['GE', 'LE', 'EQ', 8, 10, 12]:
285 # if reqf in ['LT', 'LE', 2, 10]:
290 # from yum 3.2.23, rpmUtils.miscutils, with rpmlint modifications
291 def formatRequire(name, flags, evr):
295 if flags & (rpm.RPMSENSE_LESS | rpm.RPMSENSE_GREATER |
298 if flags & rpm.RPMSENSE_LESS:
300 if flags & rpm.RPMSENSE_GREATER:
302 if flags & rpm.RPMSENSE_EQUAL:
304 s = "%s %s" % (s, versionToString(evr))
307 def versionToString(evr):
308 if not isinstance(evr, tuple) and not isinstance(evr, list):
312 if evr[0] is not None and evr[0] != "":
313 ret += str(evr[0]) + ":"
314 if evr[1] is not None:
316 if evr[2] is not None and evr[2] != "":
320 # from yum 3.2.23, rpmUtils.miscutils, with some rpmlint modifications
321 def stringToVersion(verstring):
322 if verstring in (None, ''):
323 return (None, None, None)
325 i = verstring.find(':')
328 epoch = str(long(verstring[:i]))
330 # garbage in epoch, ignore it
333 j = verstring.find('-', i)
335 if verstring[i:j] == '':
338 version = verstring[i:j]
339 release = verstring[j+1:]
341 if verstring[i:] == '':
344 version = verstring[i:]
346 return (epoch, version, release)
348 def parse_deps(line):
349 '''Parse provides/requires/conflicts/obsoletes line to list of
350 (name, flags, (epoch, version, release)) tuples.'''
353 tokens = re.split('[\s,]+', line.strip())
355 # Drop line continuation backslash in multiline macro definition (for
356 # spec file parsing), e.g.
358 # Obsoletes: foo-%1 <= 1.0.0 \
360 # (yes, this is an ugly hack and we probably have other problems with
361 # multiline macro definitions elsewhere...)
362 if tokens[-1] == '\\':
367 token = tokens.pop(0)
379 if token[0] in ("=", "<", "<=", ">", ">="):
382 flags |= rpm.RPMSENSE_EQUAL
384 flags |= rpm.RPMSENSE_LESS
386 flags |= rpm.RPMSENSE_GREATER
389 # no flags following name, treat as unversioned, add and reset
390 prco.extend((flags, (None, None, None)))
391 prcos.append(tuple(prco))
395 # last token of versioned one, add and reset
396 prco.append(stringToVersion(token))
397 prcos.append(tuple(prco))
403 prco.extend((0, (None, None, None)))
405 prco.append((None, None, None))
406 prcos.append(tuple(prco))
411 # classes representing package
415 _magic_from_compressed_re = re.compile('\([^)]+\s+compressed\s+data\\b')
417 def __init__(self, filename, dirname, header = None, is_source = False):
418 self.filename = filename
419 self.extracted = False
420 self.dirname = dirname
421 self.current_linenum = None
422 self._config_files = None
423 self._doc_files = None
424 self._noreplace_files = None
425 self._ghost_files = None
426 self._missingok_files = None
428 self._requires = None
429 self._suggests = None
430 self._supplements = None
431 self._enhances = None
432 self._recommends = None
437 self.is_source = is_source
439 # Create a package object from the file name
440 ts = rpm.TransactionSet()
441 # Don't check signatures here...
442 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
443 fd = os.open(filename, os.O_RDONLY)
445 self.header = ts.hdrFromFdno(fd)
448 self.is_source = not self.header[rpm.RPMTAG_SOURCERPM]
450 self.name = self.header[rpm.RPMTAG_NAME]
451 if self.isNoSource():
453 elif self.isSource():
456 self.arch = self.header[rpm.RPMTAG_ARCH]
458 # Return true if the package is a source package
460 return self.is_source
462 # Return true if the package is a nosource package.
463 # NoSource files are ghosts in source packages.
464 def isNoSource(self):
465 return self.is_source and self.ghostFiles()
467 # access the tags like an array
468 def __getitem__(self, key):
470 val = self.header[key]
478 # return the name of the directory where the package is extracted
480 if not self.extracted:
484 # extract rpm contents
486 s = os.stat(self.dirname)
487 if not stat.S_ISDIR(s[stat.ST_MODE]):
488 warn('Unable to access dir %s' % self.dirname)
491 self.dirname = tempfile.mkdtemp(
492 prefix = 'rpmlint.%s.' % os.path.basename(self.filename),
494 # TODO: better shell escaping or sequence based command invocation
496 'rpm2cpio "%s" | (cd "%s"; cpio -id); chmod -R +rX "%s"' % \
497 (self.filename, self.dirname, self.dirname)
498 cmd = commands.getstatusoutput(command_str)
499 self.extracted = True
502 def checkSignature(self):
503 return getstatusoutput(('env', 'LC_ALL=C', 'rpm', '-K', self.filename))
505 # remove the extracted files from the package
507 if self.extracted and self.dirname:
508 getstatusoutput(('rm', '-rf', self.dirname))
510 def grep(self, regex, filename):
511 """Grep regex from a file, return matching line numbers."""
517 in_file = open(self.dirName() + '/' + filename)
520 if regex.search(line):
521 ret.append(str(lineno))
524 Filter.printWarning(self, 'read-error', filename, e)
530 def langtag(self, tag, lang):
531 """Get value of tag in the given language."""
532 # LANGUAGE trumps other env vars per GNU gettext docs, see also #166
533 orig = os.environ.get('LANGUAGE')
534 os.environ['LANGUAGE'] = lang
537 os.environ['LANGUAGE'] = orig
540 # return the associative array indexed on file names with
541 # the values as: (file perm, file owner, file group, file link to)
543 if self._files is not None:
546 self._gatherFilesInfo()
549 # return the list of config files
550 def configFiles(self):
551 if self._config_files is not None:
552 return self._config_files
554 self._config_files = [x.name for x in self.files().values()
556 return self._config_files
558 # return the list of noreplace files
559 def noreplaceFiles(self):
560 if self._noreplace_files is not None:
561 return self._noreplace_files
563 self._noreplace_files = [x.name for x in self.files().values()
565 return self._noreplace_files
567 # return the list of documentation files
569 if self._doc_files is not None:
570 return self._doc_files
572 self._doc_files = [x.name for x in self.files().values() if x.is_doc]
573 return self._doc_files
575 # return the list of ghost files
576 def ghostFiles(self):
577 if self._ghost_files is not None:
578 return self._ghost_files
580 self._ghost_files = [x.name for x in self.files().values()
582 return self._ghost_files
584 def missingOkFiles(self):
585 if self._missingok_files is not None:
586 return self._missingok_files
588 self._missingok_files = [x.name for x in self.files().values()
590 return self._missingok_files
592 # extract information about the files
593 def _gatherFilesInfo(self):
596 flags = self.header[rpm.RPMTAG_FILEFLAGS]
597 modes = self.header[rpm.RPMTAG_FILEMODES]
598 users = self.header[rpm.RPMTAG_FILEUSERNAME]
599 groups = self.header[rpm.RPMTAG_FILEGROUPNAME]
600 links = self.header[rpm.RPMTAG_FILELINKTOS]
601 sizes = self.header[rpm.RPMTAG_FILESIZES]
602 md5s = self.header[rpm.RPMTAG_FILEMD5S]
603 mtimes = self.header[rpm.RPMTAG_FILEMTIMES]
604 rdevs = self.header[rpm.RPMTAG_FILERDEVS]
605 langs = self.header[rpm.RPMTAG_FILELANGS]
606 inodes = self.header[rpm.RPMTAG_FILEINODES]
607 requires = self.header[rpm.RPMTAG_FILEREQUIRE]
608 provides = self.header[rpm.RPMTAG_FILEPROVIDE]
609 files = self.header[rpm.RPMTAG_FILENAMES]
610 magics = self.header[rpm.RPMTAG_FILECLASS]
612 filecaps = self.header[rpm.RPMTAG_FILECAPS]
616 # rpm-python < 4.6 does not return a list for this (or FILEDEVICES,
617 # FWIW) for packages containing exactly one file
618 if not isinstance(inodes, types.ListType):
622 for idx in range(0, len(files)):
623 pkgfile = PkgFile(files[idx])
624 # Do not use os.path.join here, pkgfile.name can start with a
625 # / which would result in self.dirName being ignored
626 pkgfile.path = os.path.normpath(
627 self.dirName() + '/' + pkgfile.name)
628 pkgfile.flags = flags[idx]
629 pkgfile.mode = modes[idx]
630 pkgfile.user = users[idx]
631 pkgfile.group = groups[idx]
632 pkgfile.linkto = links[idx] and safe_normpath(links[idx])
633 pkgfile.size = sizes[idx]
634 pkgfile.md5 = md5s[idx]
635 pkgfile.mtime = mtimes[idx]
636 pkgfile.rdev = rdevs[idx]
637 pkgfile.inode = inodes[idx]
638 pkgfile.requires = parse_deps(requires[idx])
639 pkgfile.provides = parse_deps(provides[idx])
640 pkgfile.lang = langs[idx]
641 pkgfile.magic = magics[idx]
642 if not pkgfile.magic and _magic:
643 pkgfile.magic = _magic.file(pkgfile.path)
644 if pkgfile.magic is None:
646 elif Pkg._magic_from_compressed_re.search(pkgfile.magic):
647 # Discard magic from inside compressed files ("file -z")
648 # until PkgFile gets decompression support. We may get
649 # such magic strings from package headers already now;
650 # for example Fedora's rpmbuild as of F-11's 4.7.1 is
651 # patched so it generates them.
654 pkgfile.filecaps = filecaps[idx]
655 self._files[pkgfile.name] = pkgfile
657 def readlink(self, pkgfile):
658 """Resolve symlinks for the given PkgFile, return the dereferenced
659 PkgFile if it is found in this package, None if not."""
661 while result and result.linkto:
662 linkpath = urlparse.urljoin(result.name, result.linkto)
663 linkpath = safe_normpath(linkpath)
664 result = self.files().get(linkpath)
667 # API to access dependency information
669 """Get package Obsoletes as list of
670 (name, flags, (epoch, version, release)) tuples."""
671 self._gatherDepInfo()
672 return self._obsoletes
675 """Get package Requires as list of
676 (name, flags, (epoch, version, release)) tuples."""
677 self._gatherDepInfo()
678 return self._requires
680 def recommends(self):
681 self._gatherDepInfo()
682 return self._recommends
685 self._gatherDepInfo()
686 return self._suggests
688 def supplements(self):
689 self._gatherDepInfo()
690 return self._supplements
693 self._gatherDepInfo()
694 return self._enhances
697 """Get package PreReqs as list of
698 (name, flags, (epoch, version, release)) tuples."""
699 self._gatherDepInfo()
703 if self._req_names == -1:
704 self._req_names = [x[0] for x in self.requires() + self.prereq()]
705 return self._req_names
707 def check_versioned_dep(self, name, version):
708 # try to match name%_isa as well (e.g. "foo(x86-64)", "foo(x86-32)")
709 name_re = re.compile('^%s(\(\w+-\d+\))?$' % re.escape(name))
710 for d in self.requires() + self.prereq():
711 if name_re.match(d[0]):
712 if d[1] & rpm.RPMSENSE_EQUAL != rpm.RPMSENSE_EQUAL \
713 or d[2][1] != version:
719 """Get package Conflicts as list of
720 (name, flags, (epoch, version, release)) tuples."""
721 self._gatherDepInfo()
722 return self._conflicts
725 """Get package Provides as list of
726 (name, flags, (epoch, version, release)) tuples."""
727 self._gatherDepInfo()
728 return self._provides
730 # internal function to gather dependency info used by the above ones
731 def _gather_aux(self, header, list, nametag, flagstag, versiontag,
732 prereq = None, strong_only = False, weak_only = False):
733 names = header[nametag]
734 flags = header[flagstag]
735 versions = header[versiontag]
738 for loop in range(len(versions)):
739 evr = stringToVersion(versions[loop])
740 if prereq is not None and flags[loop] & PREREQ_FLAG:
741 prereq.append((names[loop], flags[loop] & (~PREREQ_FLAG),
743 elif strong_only and flags[loop] & rpm.RPMSENSE_STRONG:
744 list.append((names[loop], versions[loop], flags[loop] & (~rpm.RPMSENSE_STRONG)))
745 elif weak_only and not (flags[loop] & rpm.RPMSENSE_STRONG):
746 list.append((names[loop], versions[loop], flags[loop]))
747 elif not (weak_only or strong_only):
748 list.append((names[loop], flags[loop], evr))
750 def _gatherDepInfo(self):
751 if self._requires is None:
758 self._supplements = []
760 self._recommends = []
762 self._gather_aux(self.header, self._requires,
763 rpm.RPMTAG_REQUIRENAME,
764 rpm.RPMTAG_REQUIREFLAGS,
765 rpm.RPMTAG_REQUIREVERSION,
767 self._gather_aux(self.header, self._conflicts,
768 rpm.RPMTAG_CONFLICTNAME,
769 rpm.RPMTAG_CONFLICTFLAGS,
770 rpm.RPMTAG_CONFLICTVERSION)
771 self._gather_aux(self.header, self._provides,
772 rpm.RPMTAG_PROVIDENAME,
773 rpm.RPMTAG_PROVIDEFLAGS,
774 rpm.RPMTAG_PROVIDEVERSION)
775 self._gather_aux(self.header, self._obsoletes,
776 rpm.RPMTAG_OBSOLETENAME,
777 rpm.RPMTAG_OBSOLETEFLAGS,
778 rpm.RPMTAG_OBSOLETEVERSION)
780 self._gather_aux(self.header, self._recommends,
781 rpm.RPMTAG_SUGGESTSNAME,
782 rpm.RPMTAG_SUGGESTSFLAGS,
783 rpm.RPMTAG_SUGGESTSVERSION,
785 self._gather_aux(self.header, self._suggests,
786 rpm.RPMTAG_SUGGESTSNAME,
787 rpm.RPMTAG_SUGGESTSFLAGS,
788 rpm.RPMTAG_SUGGESTSVERSION,
790 self._gather_aux(self.header, self._supplements,
791 rpm.RPMTAG_ENHANCESNAME,
792 rpm.RPMTAG_ENHANCESFLAGS,
793 rpm.RPMTAG_ENHANCESVERSION,
795 self._gather_aux(self.header, self._enhances,
796 rpm.RPMTAG_ENHANCESNAME,
797 rpm.RPMTAG_ENHANCESFLAGS,
798 rpm.RPMTAG_ENHANCESVERSION,
804 def scriptprog(self, which):
805 """Get the specified script interpreter as a string.
806 Depending on rpm-python version, the string may or may not include
807 interpreter arguments, if any."""
811 elif not isinstance(prog, basestring):
812 # http://rpm.org/ticket/847#comment:2
813 prog = " ".join(prog)
817 def getInstalledPkgs(name):
818 """Get list of installed package objects by name."""
821 ts = rpm.TransactionSet()
822 if re.search('[?*]|\[.+\]', name):
824 mi.pattern("name", rpm.RPMMIRE_GLOB, name)
826 mi = ts.dbMatch("name", name)
829 pkgs.append(InstalledPkg(name, hdr))
833 # Class to provide an API to an installed package
834 class InstalledPkg(Pkg):
835 def __init__(self, name, hdr = None):
837 ts = rpm.TransactionSet()
838 mi = ts.dbMatch('name', name)
843 except StopIteration:
846 Pkg.__init__(self, name, '/', hdr)
848 self.extracted = True
849 # create a fake filename to satisfy some checks on the filename
850 self.filename = '%s-%s-%s.%s.rpm' % \
851 (self.name, self[rpm.RPMTAG_VERSION], self[rpm.RPMTAG_RELEASE],
852 self[rpm.RPMTAG_ARCH])
857 def checkSignature(self):
858 return (0, 'fake: pgp md5 OK')
860 # Class to provide an API to a "fake" package, eg. for specfile-only checks
862 def __init__(self, name):
865 self.current_linenum = None
870 # Class for files in packages
871 class PkgFile(object):
873 def __init__(self, name):
875 # Real path to the file (taking extract dir into account)
893 # TODO: decompression support
895 is_config = property(lambda self: self.flags & rpm.RPMFILE_CONFIG)
896 is_doc = property(lambda self: self.flags & rpm.RPMFILE_DOC)
897 is_noreplace = property(lambda self: self.flags & rpm.RPMFILE_NOREPLACE)
898 is_ghost = property(lambda self: self.flags & rpm.RPMFILE_GHOST)
899 is_missingok = property(lambda self: self.flags & rpm.RPMFILE_MISSINGOK)
902 if __name__ == '__main__':
903 for p in sys.argv[1:]:
904 pkg = Pkg(sys.argv[1], tempfile.gettempdir())
905 print ('Requires: %s' % pkg.requires())
906 print ('Prereq: %s' % pkg.prereq())
907 print ('Conflicts: %s' % pkg.conflicts())
908 print ('Provides: %s' % pkg.provides())
909 print ('Obsoletes: %s' % pkg.obsoletes())
915 # indent-tabs-mode: nil
916 # py-indent-offset: 4