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
433 self.is_source = is_source
435 # Create a package object from the file name
436 ts = rpm.TransactionSet()
437 # Don't check signatures here...
438 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
439 fd = os.open(filename, os.O_RDONLY)
441 self.header = ts.hdrFromFdno(fd)
444 self.is_source = not self.header[rpm.RPMTAG_SOURCERPM]
446 self.name = self.header[rpm.RPMTAG_NAME]
447 if self.isNoSource():
449 elif self.isSource():
452 self.arch = self.header[rpm.RPMTAG_ARCH]
454 # Return true if the package is a source package
456 return self.is_source
458 # Return true if the package is a nosource package.
459 # NoSource files are ghosts in source packages.
460 def isNoSource(self):
461 return self.is_source and self.ghostFiles()
463 # access the tags like an array
464 def __getitem__(self, key):
466 val = self.header[key]
474 # return the name of the directory where the package is extracted
476 if not self.extracted:
480 # extract rpm contents
482 s = os.stat(self.dirname)
483 if not stat.S_ISDIR(s[stat.ST_MODE]):
484 warn('Unable to access dir %s' % self.dirname)
487 self.dirname = tempfile.mkdtemp(
488 prefix = 'rpmlint.%s.' % os.path.basename(self.filename),
490 # TODO: better shell escaping or sequence based command invocation
492 'rpm2cpio "%s" | (cd "%s"; cpio -id); chmod -R +rX "%s"' % \
493 (self.filename, self.dirname, self.dirname)
494 cmd = commands.getstatusoutput(command_str)
495 self.extracted = True
498 def checkSignature(self):
499 return getstatusoutput(('env', 'LC_ALL=C', 'rpm', '-K', self.filename))
501 # remove the extracted files from the package
503 if self.extracted and self.dirname:
504 getstatusoutput(('rm', '-rf', self.dirname))
506 def grep(self, regex, filename):
507 """Grep regex from a file, return matching line numbers."""
513 in_file = open(self.dirName() + '/' + filename)
516 if regex.search(line):
517 ret.append(str(lineno))
520 Filter.printWarning(self, 'read-error', filename, e)
526 def langtag(self, tag, lang):
527 """Get value of tag in the given language."""
528 # LANGUAGE trumps other env vars per GNU gettext docs, see also #166
529 orig = os.environ.get('LANGUAGE')
530 os.environ['LANGUAGE'] = lang
533 os.environ['LANGUAGE'] = orig
536 # return the associative array indexed on file names with
537 # the values as: (file perm, file owner, file group, file link to)
539 if self._files is not None:
542 self._gatherFilesInfo()
545 # return the list of config files
546 def configFiles(self):
547 if self._config_files is not None:
548 return self._config_files
550 self._config_files = [x.name for x in self.files().values()
552 return self._config_files
554 # return the list of noreplace files
555 def noreplaceFiles(self):
556 if self._noreplace_files is not None:
557 return self._noreplace_files
559 self._noreplace_files = [x.name for x in self.files().values()
561 return self._noreplace_files
563 # return the list of documentation files
565 if self._doc_files is not None:
566 return self._doc_files
568 self._doc_files = [x.name for x in self.files().values() if x.is_doc]
569 return self._doc_files
571 # return the list of ghost files
572 def ghostFiles(self):
573 if self._ghost_files is not None:
574 return self._ghost_files
576 self._ghost_files = [x.name for x in self.files().values()
578 return self._ghost_files
580 def missingOkFiles(self):
581 if self._missingok_files is not None:
582 return self._missingok_files
584 self._missingok_files = [x.name for x in self.files().values()
586 return self._missingok_files
588 # extract information about the files
589 def _gatherFilesInfo(self):
592 flags = self.header[rpm.RPMTAG_FILEFLAGS]
593 modes = self.header[rpm.RPMTAG_FILEMODES]
594 users = self.header[rpm.RPMTAG_FILEUSERNAME]
595 groups = self.header[rpm.RPMTAG_FILEGROUPNAME]
596 links = self.header[rpm.RPMTAG_FILELINKTOS]
597 sizes = self.header[rpm.RPMTAG_FILESIZES]
598 md5s = self.header[rpm.RPMTAG_FILEMD5S]
599 mtimes = self.header[rpm.RPMTAG_FILEMTIMES]
600 rdevs = self.header[rpm.RPMTAG_FILERDEVS]
601 langs = self.header[rpm.RPMTAG_FILELANGS]
602 inodes = self.header[rpm.RPMTAG_FILEINODES]
603 requires = self.header[rpm.RPMTAG_FILEREQUIRE]
604 provides = self.header[rpm.RPMTAG_FILEPROVIDE]
605 files = self.header[rpm.RPMTAG_FILENAMES]
606 magics = self.header[rpm.RPMTAG_FILECLASS]
608 filecaps = self.header[rpm.RPMTAG_FILECAPS]
612 # rpm-python < 4.6 does not return a list for this (or FILEDEVICES,
613 # FWIW) for packages containing exactly one file
614 if not isinstance(inodes, types.ListType):
618 for idx in range(0, len(files)):
619 pkgfile = PkgFile(files[idx])
620 # Do not use os.path.join here, pkgfile.name can start with a
621 # / which would result in self.dirName being ignored
622 pkgfile.path = os.path.normpath(
623 self.dirName() + '/' + pkgfile.name)
624 pkgfile.flags = flags[idx]
625 pkgfile.mode = modes[idx]
626 pkgfile.user = users[idx]
627 pkgfile.group = groups[idx]
628 pkgfile.linkto = links[idx] and safe_normpath(links[idx])
629 pkgfile.size = sizes[idx]
630 pkgfile.md5 = md5s[idx]
631 pkgfile.mtime = mtimes[idx]
632 pkgfile.rdev = rdevs[idx]
633 pkgfile.inode = inodes[idx]
634 pkgfile.requires = parse_deps(requires[idx])
635 pkgfile.provides = parse_deps(provides[idx])
636 pkgfile.lang = langs[idx]
637 pkgfile.magic = magics[idx]
638 if not pkgfile.magic and _magic:
639 pkgfile.magic = _magic.file(pkgfile.path)
640 if pkgfile.magic is None:
642 elif Pkg._magic_from_compressed_re.search(pkgfile.magic):
643 # Discard magic from inside compressed files ("file -z")
644 # until PkgFile gets decompression support. We may get
645 # such magic strings from package headers already now;
646 # for example Fedora's rpmbuild as of F-11's 4.7.1 is
647 # patched so it generates them.
650 pkgfile.filecaps = filecaps[idx]
651 self._files[pkgfile.name] = pkgfile
653 def readlink(self, pkgfile):
654 """Resolve symlinks for the given PkgFile, return the dereferenced
655 PkgFile if it is found in this package, None if not."""
657 while result and result.linkto:
658 linkpath = urlparse.urljoin(result.name, result.linkto)
659 linkpath = safe_normpath(linkpath)
660 result = self.files().get(linkpath)
663 # API to access dependency information
665 """Get package Obsoletes as list of
666 (name, flags, (epoch, version, release)) tuples."""
667 self._gatherDepInfo()
668 return self._obsoletes
671 """Get package Requires as list of
672 (name, flags, (epoch, version, release)) tuples."""
673 self._gatherDepInfo()
674 return self._requires
677 """Get package PreReqs as list of
678 (name, flags, (epoch, version, release)) tuples."""
679 self._gatherDepInfo()
683 if self._req_names == -1:
684 self._req_names = [x[0] for x in self.requires() + self.prereq()]
685 return self._req_names
687 def check_versioned_dep(self, name, version):
688 # try to match name%_isa as well (e.g. "foo(x86-64)", "foo(x86-32)")
689 name_re = re.compile('^%s(\(\w+-\d+\))?$' % re.escape(name))
690 for d in self.requires() + self.prereq():
691 if name_re.match(d[0]):
692 if d[1] & rpm.RPMSENSE_EQUAL != rpm.RPMSENSE_EQUAL \
693 or d[2][1] != version:
699 """Get package Conflicts as list of
700 (name, flags, (epoch, version, release)) tuples."""
701 self._gatherDepInfo()
702 return self._conflicts
705 """Get package Provides as list of
706 (name, flags, (epoch, version, release)) tuples."""
707 self._gatherDepInfo()
708 return self._provides
710 # internal function to gather dependency info used by the above ones
711 def _gather_aux(self, header, list, nametag, flagstag, versiontag,
713 names = header[nametag]
714 flags = header[flagstag]
715 versions = header[versiontag]
718 for loop in range(len(versions)):
719 evr = stringToVersion(versions[loop])
720 if prereq is not None and flags[loop] & PREREQ_FLAG:
721 prereq.append((names[loop], flags[loop] & (~PREREQ_FLAG),
724 list.append((names[loop], flags[loop], evr))
726 def _gatherDepInfo(self):
727 if self._requires is None:
734 self._gather_aux(self.header, self._requires,
735 rpm.RPMTAG_REQUIRENAME,
736 rpm.RPMTAG_REQUIREFLAGS,
737 rpm.RPMTAG_REQUIREVERSION,
739 self._gather_aux(self.header, self._conflicts,
740 rpm.RPMTAG_CONFLICTNAME,
741 rpm.RPMTAG_CONFLICTFLAGS,
742 rpm.RPMTAG_CONFLICTVERSION)
743 self._gather_aux(self.header, self._provides,
744 rpm.RPMTAG_PROVIDENAME,
745 rpm.RPMTAG_PROVIDEFLAGS,
746 rpm.RPMTAG_PROVIDEVERSION)
747 self._gather_aux(self.header, self._obsoletes,
748 rpm.RPMTAG_OBSOLETENAME,
749 rpm.RPMTAG_OBSOLETEFLAGS,
750 rpm.RPMTAG_OBSOLETEVERSION)
752 def scriptprog(self, which):
753 """Get the specified script interpreter as a string.
754 Depending on rpm-python version, the string may or may not include
755 interpreter arguments, if any."""
759 elif not isinstance(prog, basestring):
760 # http://rpm.org/ticket/847#comment:2
761 prog = " ".join(prog)
764 def getInstalledPkgs(name):
765 """Get list of installed package objects by name."""
768 ts = rpm.TransactionSet()
769 if re.search('[?*]|\[.+\]', name):
771 mi.pattern("name", rpm.RPMMIRE_GLOB, name)
773 mi = ts.dbMatch("name", name)
776 pkgs.append(InstalledPkg(name, hdr))
780 # Class to provide an API to an installed package
781 class InstalledPkg(Pkg):
782 def __init__(self, name, hdr = None):
784 ts = rpm.TransactionSet()
785 mi = ts.dbMatch('name', name)
790 except StopIteration:
793 Pkg.__init__(self, name, '/', hdr)
795 self.extracted = True
796 # create a fake filename to satisfy some checks on the filename
797 self.filename = '%s-%s-%s.%s.rpm' % \
798 (self.name, self[rpm.RPMTAG_VERSION], self[rpm.RPMTAG_RELEASE],
799 self[rpm.RPMTAG_ARCH])
804 def checkSignature(self):
805 return (0, 'fake: pgp md5 OK')
807 # Class to provide an API to a "fake" package, eg. for specfile-only checks
809 def __init__(self, name):
812 self.current_linenum = None
817 # Class for files in packages
818 class PkgFile(object):
820 def __init__(self, name):
822 # Real path to the file (taking extract dir into account)
840 # TODO: decompression support
842 is_config = property(lambda self: self.flags & rpm.RPMFILE_CONFIG)
843 is_doc = property(lambda self: self.flags & rpm.RPMFILE_DOC)
844 is_noreplace = property(lambda self: self.flags & rpm.RPMFILE_NOREPLACE)
845 is_ghost = property(lambda self: self.flags & rpm.RPMFILE_GHOST)
846 is_missingok = property(lambda self: self.flags & rpm.RPMFILE_MISSINGOK)
849 if __name__ == '__main__':
850 for p in sys.argv[1:]:
851 pkg = Pkg(sys.argv[1], tempfile.gettempdir())
852 print ('Requires: %s' % pkg.requires())
853 print ('Prereq: %s' % pkg.prereq())
854 print ('Conflicts: %s' % pkg.conflicts())
855 print ('Provides: %s' % pkg.provides())
856 print ('Obsoletes: %s' % pkg.obsoletes())
862 # indent-tabs-mode: nil
863 # py-indent-offset: 4