#!/usr/bin/env python # # ---------------------------------------------------------------------------- # "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp): # Maxim Sobolev wrote this file. As long as you retain # this notice you can do whatever you want with this stuff. If we meet some # day, and you think this stuff is worth it, you can buy me a beer in return. # # Maxim Sobolev # ---------------------------------------------------------------------------- # # $FreeBSD: branches/pkg_install/Tools/scripts/chkdepschain.py 340851 2014-01-23 19:55:14Z mat $ # # MAINTAINER= sobomax@FreeBSD.org <- any unapproved commits to this file are # highly discouraged!!! # import os, os.path, popen2, types, sys, getopt, pickle # Global constants and semi-constants PKG_DBDIR = '/var/db/pkg' PORTSDIR = '/usr/ports' ROOT_PORTMK = '/usr/share/mk/bsd.port.mk' PLIST_FILE = '+CONTENTS' ORIGIN_PREF = '@comment ORIGIN:' MAKEFILE = 'Makefile' MAKE = 'make' ERR_PREF = 'Error:' WARN_PREF = 'Warning:' # Global variables # # PortInfo cache picache = {} # Useful aliases op_isdir = os.path.isdir op_join = os.path.join op_split = os.path.split op_abspath = os.path.abspath # # Query origin of specified installed package. # def getorigin(pkg): plist = op_join(PKG_DBDIR, pkg, PLIST_FILE) for line in open(plist).xreadlines(): if line.startswith(ORIGIN_PREF): origin = line[len(ORIGIN_PREF):].strip() break else: raise RuntimeError('%s: no origin recorded' % plist) return origin # # Execute external command and return content of its stdout. # def getcmdout(cmdline, filterempty = 1): pipe = popen2.Popen3(cmdline, 1) results = pipe.fromchild.readlines() for stream in (pipe.fromchild, pipe.tochild, pipe.childerr): stream.close() if pipe.wait() != 0: if type(cmdline) is types.StringType: cmdline = (cmdline) raise IOError('%s: external command returned non-zero error code' % \ cmdline[0]) if filterempty != 0: results = filter(lambda line: len(line.strip()) > 0, results) return results # # For a specified path (either dir or makefile) query requested make(1) # variables and return them as a tuple in exactly the same order as they # were specified in function call, i.e. querymakevars('foo', 'A', 'B') will # return a tuple with a first element being the value of A variable, and # the second one - the value of B. # def querymakevars(path, *vars): if op_isdir(path): path = op_join(path, MAKEFILE) dirname, makefile = op_split(path) cmdline = [MAKE, '-f', makefile] savedir = os.getcwd() os.chdir(dirname) try: for var in vars: cmdline.extend(('-V', var)) results = map(lambda line: line.strip(), getcmdout(cmdline, 0)) finally: os.chdir(savedir) return tuple(results) def parsedeps(depstr): return tuple(map(lambda dep: dep.split(':'), depstr.split())) # # For a specified port return either a new instance of the PortInfo class, # or existing instance from the cache. # def getpi(path): path = op_abspath(path) if not picache.has_key(path): picache[path] = PortInfo(path) return picache[path] # # Format text string according to requested constrains. Useful when you have # to display multi-line, variable width message on terminal. # def formatmsg(msg, wrapat = 78, seclindent = 0): words = msg.split() result = '' position = 0 for word in words: if position + 1 + len(word) > wrapat: result += '\n' + ' ' * seclindent + word position = seclindent + len(word) else: if position != 0: result += ' ' position += 1 result += word position += len(word) return result # # Class that contain main info about the port # class PortInfo: PKGNAME = None CATEGORIES = None MAINTAINER = None BUILD_DEPENDS = None LIB_DEPENDS = None RUN_DEPENDS = None PKGORIGIN = None # Cached values, to speed-up things __deps = None __bt_deps = None __rt_deps = None def __init__(self, path): self.PKGNAME, self.CATEGORIES, self.MAINTAINER, self.BUILD_DEPENDS, \ self.LIB_DEPENDS, self.RUN_DEPENDS, self.PKGORIGIN = \ querymakevars(path, 'PKGNAME', 'CATEGORIES', 'MAINTAINER', \ 'BUILD_DEPENDS', 'LIB_DEPENDS', 'RUN_DEPENDS', 'PKGORIGIN') def __str__(self): return 'PKGNAME:\t%s\nCATEGORIES:\t%s\nMAINTAINER:\t%s\n' \ 'BUILD_DEPENDS:\t%s\nLIB_DEPENDS:\t%s\nRUN_DEPENDS:\t%s\n' \ 'PKGORIGIN:\t%s' % (self.PKGNAME, self.CATEGORIES, self.MAINTAINER, \ self.BUILD_DEPENDS, self.LIB_DEPENDS, self.RUN_DEPENDS, \ self.PKGORIGIN) def getdeps(self): if self.__deps == None: result = [] for depstr in self.BUILD_DEPENDS, self.LIB_DEPENDS, \ self.RUN_DEPENDS: deps = tuple(map(lambda dep: dep[1], parsedeps(depstr))) result.append(deps) self.__deps = tuple(result) return self.__deps def get_bt_deps(self): if self.__bt_deps == None: topdeps = self.getdeps() topdeps = list(topdeps[0] + topdeps[1]) for dep in topdeps[:]: botdeps = filter(lambda dep: dep not in topdeps, \ getpi(dep).get_rt_deps()) topdeps.extend(botdeps) self.__bt_deps = tuple(topdeps) return self.__bt_deps def get_rt_deps(self): if self.__rt_deps == None: topdeps = self.getdeps() topdeps = list(topdeps[1] + topdeps[2]) for dep in topdeps[:]: botdeps = filter(lambda dep: dep not in topdeps, \ getpi(dep).get_rt_deps()) topdeps.extend(botdeps) self.__rt_deps = tuple(topdeps) return self.__rt_deps def write_msg(*message): if type(message) == types.StringType: message = message, message = tuple(filter(lambda line: line != None, message)) sys.stderr.writelines(message) # # Print optional message and usage information and exit with specified exit # code. # def usage(code, msg = None): myname = os.path.basename(sys.argv[0]) if msg != None: msg = str(msg) + '\n' write_msg(msg, "Usage: %s [-rb] [-l|L cachefile] [-s cachefile]\n" % \ myname) sys.exit(code) def main(): global picache # Parse command line arguments try: opts, args = getopt.getopt(sys.argv[1:], 'erbl:L:s:') except getopt.GetoptError, msg: usage(2, msg) if len(args) > 0 or len(opts) == 0 : usage(2) cachefile = None chk_bt_deps = 0 chk_rt_deps = 0 warn_as_err = 0 for o, a in opts: if o == '-b': chk_bt_deps = 1 elif o == '-r': chk_rt_deps = 1 elif o in ('-l', '-L'): # Try to load saved PortInfo cache try: picache = pickle.load(open(a)) except: picache = {} try: if o == '-L': os.unlink(a) except: pass elif o == '-s': cachefile = a elif o == '-e': warn_as_err = 1 # Load origins of all installed packages instpkgs = os.listdir(PKG_DBDIR) instpkgs = filter(lambda pkg: op_isdir(op_join(PKG_DBDIR, pkg)), instpkgs) origins = {} for pkg in instpkgs: origins[pkg] = getorigin(pkg) # Resolve dependencies for the current port info = getpi(os.getcwd()) deps = [] if chk_bt_deps != 0: deps.extend(filter(lambda d: d not in deps, info.get_bt_deps())) if chk_rt_deps != 0: deps.extend(filter(lambda d: d not in deps, info.get_rt_deps())) # Perform validation nerrs = 0 nwarns = 0 if warn_as_err == 0: warn_pref = WARN_PREF else: warn_pref = ERR_PREF err_pref = ERR_PREF for dep in deps: pi = getpi(dep) if pi.PKGORIGIN not in origins.values(): print formatmsg(seclindent = 7 * 0, msg = \ '%s package %s (%s) belongs to dependency chain, but ' \ 'isn\'t installed.' % (err_pref, pi.PKGNAME, pi.PKGORIGIN)) nerrs += 1 elif pi.PKGNAME not in origins.keys(): for instpkg in origins.keys(): if origins[instpkg] == pi.PKGORIGIN: break print formatmsg(seclindent = 9 * 0, msg = \ '%s package %s (%s) belongs to dependency chain, but ' \ 'package %s is installed instead. Perhaps it\'s an older ' \ 'version - update is highly recommended.' % (warn_pref, \ pi.PKGNAME, pi.PKGORIGIN, instpkg)) nwarns += 1 # Save PortInfo cache if requested if cachefile != None: try: pickle.dump(picache, open(cachefile, 'w')) except: pass if warn_as_err != 0: nerrs += nwarns return nerrs PORTSDIR, PKG_DBDIR = querymakevars(ROOT_PORTMK, 'PORTSDIR', 'PKG_DBDIR') if __name__ == '__main__': try: sys.exit(main()) except KeyboardInterrupt: pass