320 lines
9.1 KiB
Python
Executable file
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
|