freebsd-ports/Tools/scripts/chkdepschain.py
2005-01-09 10:21:17 +00:00

320 lines
9.1 KiB
Python
Executable file

#!/usr/local/bin/python
#
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp):
# Maxim Sobolev <sobomax@FreeBSD.org> 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$
#
# 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