1#!/usr/local/bin/python 2# 3# ---------------------------------------------------------------------------- 4# "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp): 5# Maxim Sobolev <sobomax@FreeBSD.org> wrote this file. As long as you retain 6# this notice you can do whatever you want with this stuff. If we meet some 7# day, and you think this stuff is worth it, you can buy me a beer in return. 8# 9# Maxim Sobolev 10# ---------------------------------------------------------------------------- 11# 12 13import os, os.path, popen2, types, sys, getopt, pickle 14 15# Global constants and semi-constants 16PKG_DBDIR = '/var/db/pkg' 17PORTSDIR = '/usr/mports' 18ROOT_PORTMK = '/usr/share/mk/bsd.port.mk' 19PLIST_FILE = '+CONTENTS' 20ORIGIN_PREF = '@comment ORIGIN:' 21MAKEFILE = 'Makefile' 22MAKE = 'make' 23ERR_PREF = 'Error:' 24WARN_PREF = 'Warning:' 25 26# Global variables 27# 28# PortInfo cache 29picache = {} 30 31# Useful aliases 32op_isdir = os.path.isdir 33op_join = os.path.join 34op_split = os.path.split 35op_abspath = os.path.abspath 36 37 38# 39# Query origin of specified installed package. 40# 41def getorigin(pkg): 42 plist = op_join(PKG_DBDIR, pkg, PLIST_FILE) 43 for line in open(plist).xreadlines(): 44 if line.startswith(ORIGIN_PREF): 45 origin = line[len(ORIGIN_PREF):].strip() 46 break 47 else: 48 raise RuntimeError('%s: no origin recorded' % plist) 49 50 return origin 51 52 53# 54# Execute external command and return content of its stdout. 55# 56def getcmdout(cmdline, filterempty = 1): 57 pipe = popen2.Popen3(cmdline, 1) 58 results = pipe.fromchild.readlines() 59 for stream in (pipe.fromchild, pipe.tochild, pipe.childerr): 60 stream.close() 61 62 if pipe.wait() != 0: 63 if type(cmdline) is types.StringType: 64 cmdline = (cmdline) 65 raise IOError('%s: external command returned non-zero error code' % \ 66 cmdline[0]) 67 68 if filterempty != 0: 69 results = filter(lambda line: len(line.strip()) > 0, results) 70 71 return results 72 73 74# 75# For a specified path (either dir or makefile) query requested make(1) 76# variables and return them as a tuple in exactly the same order as they 77# were specified in function call, i.e. querymakevars('foo', 'A', 'B') will 78# return a tuple with a first element being the value of A variable, and 79# the second one - the value of B. 80# 81def querymakevars(path, *vars): 82 if op_isdir(path): 83 path = op_join(path, MAKEFILE) 84 dirname, makefile = op_split(path) 85 cmdline = [MAKE, '-f', makefile] 86 savedir = os.getcwd() 87 os.chdir(dirname) 88 try: 89 for var in vars: 90 cmdline.extend(('-V', var)) 91 92 results = map(lambda line: line.strip(), getcmdout(cmdline, 0)) 93 finally: 94 os.chdir(savedir) 95 96 return tuple(results) 97 98 99def parsedeps(depstr): 100 return tuple(map(lambda dep: dep.split(':'), depstr.split())) 101 102 103# 104# For a specified port return either a new instance of the PortInfo class, 105# or existing instance from the cache. 106# 107def getpi(path): 108 path = op_abspath(path) 109 if not picache.has_key(path): 110 picache[path] = PortInfo(path) 111 return picache[path] 112 113 114# 115# Format text string according to requested constrains. Useful when you have 116# to display multi-line, variable width message on terminal. 117# 118def formatmsg(msg, wrapat = 78, seclindent = 0): 119 words = msg.split() 120 result = '' 121 position = 0 122 for word in words: 123 if position + 1 + len(word) > wrapat: 124 result += '\n' + ' ' * seclindent + word 125 position = seclindent + len(word) 126 else: 127 if position != 0: 128 result += ' ' 129 position += 1 130 result += word 131 position += len(word) 132 133 return result 134 135 136# 137# Class that contain main info about the port 138# 139class PortInfo: 140 PKGNAME = None 141 CATEGORIES = None 142 MAINTAINER = None 143 BUILD_DEPENDS = None 144 LIB_DEPENDS = None 145 RUN_DEPENDS = None 146 PKGORIGIN = None 147 # Cached values, to speed-up things 148 __deps = None 149 __bt_deps = None 150 __rt_deps = None 151 152 def __init__(self, path): 153 self.PKGNAME, self.CATEGORIES, self.MAINTAINER, self.BUILD_DEPENDS, \ 154 self.LIB_DEPENDS, self.RUN_DEPENDS, self.PKGORIGIN = \ 155 querymakevars(path, 'PKGNAME', 'CATEGORIES', 'MAINTAINER', \ 156 'BUILD_DEPENDS', 'LIB_DEPENDS', 'RUN_DEPENDS', 'PKGORIGIN') 157 158 def __str__(self): 159 return 'PKGNAME:\t%s\nCATEGORIES:\t%s\nMAINTAINER:\t%s\n' \ 160 'BUILD_DEPENDS:\t%s\nLIB_DEPENDS:\t%s\nRUN_DEPENDS:\t%s\n' \ 161 'PKGORIGIN:\t%s' % (self.PKGNAME, self.CATEGORIES, self.MAINTAINER, \ 162 self.BUILD_DEPENDS, self.LIB_DEPENDS, self.RUN_DEPENDS, \ 163 self.PKGORIGIN) 164 165 def getdeps(self): 166 if self.__deps == None: 167 result = [] 168 for depstr in self.BUILD_DEPENDS, self.LIB_DEPENDS, \ 169 self.RUN_DEPENDS: 170 deps = tuple(map(lambda dep: dep[1], parsedeps(depstr))) 171 result.append(deps) 172 self.__deps = tuple(result) 173 return self.__deps 174 175 def get_bt_deps(self): 176 if self.__bt_deps == None: 177 topdeps = self.getdeps() 178 topdeps = list(topdeps[0] + topdeps[1]) 179 for dep in topdeps[:]: 180 botdeps = filter(lambda dep: dep not in topdeps, \ 181 getpi(dep).get_rt_deps()) 182 topdeps.extend(botdeps) 183 self.__bt_deps = tuple(topdeps) 184 return self.__bt_deps 185 186 def get_rt_deps(self): 187 if self.__rt_deps == None: 188 topdeps = self.getdeps() 189 topdeps = list(topdeps[1] + topdeps[2]) 190 for dep in topdeps[:]: 191 botdeps = filter(lambda dep: dep not in topdeps, \ 192 getpi(dep).get_rt_deps()) 193 topdeps.extend(botdeps) 194 self.__rt_deps = tuple(topdeps) 195 return self.__rt_deps 196 197 198def write_msg(*message): 199 if type(message) == types.StringType: 200 message = message, 201 message = tuple(filter(lambda line: line != None, message)) 202 sys.stderr.writelines(message) 203 204 205# 206# Print optional message and usage information and exit with specified exit 207# code. 208# 209def usage(code, msg = None): 210 myname = os.path.basename(sys.argv[0]) 211 if msg != None: 212 msg = str(msg) + '\n' 213 write_msg(msg, "Usage: %s [-rb] [-l|L cachefile] [-s cachefile]\n" % \ 214 myname) 215 sys.exit(code) 216 217 218def main(): 219 global picache 220 221 # Parse command line arguments 222 try: 223 opts, args = getopt.getopt(sys.argv[1:], 'erbl:L:s:') 224 except getopt.GetoptError, msg: 225 usage(2, msg) 226 227 if len(args) > 0 or len(opts) == 0 : 228 usage(2) 229 230 cachefile = None 231 chk_bt_deps = 0 232 chk_rt_deps = 0 233 warn_as_err = 0 234 for o, a in opts: 235 if o == '-b': 236 chk_bt_deps = 1 237 elif o == '-r': 238 chk_rt_deps = 1 239 elif o in ('-l', '-L'): 240 # Try to load saved PortInfo cache 241 try: 242 picache = pickle.load(open(a)) 243 except: 244 picache = {} 245 try: 246 if o == '-L': 247 os.unlink(a) 248 except: 249 pass 250 elif o == '-s': 251 cachefile = a 252 elif o == '-e': 253 warn_as_err = 1 254 255 # Load origins of all installed packages 256 instpkgs = os.listdir(PKG_DBDIR) 257 instpkgs = filter(lambda pkg: op_isdir(op_join(PKG_DBDIR, pkg)), instpkgs) 258 origins = {} 259 for pkg in instpkgs: 260 origins[pkg] = getorigin(pkg) 261 262 # Resolve dependencies for the current port 263 info = getpi(os.getcwd()) 264 deps = [] 265 if chk_bt_deps != 0: 266 deps.extend(filter(lambda d: d not in deps, info.get_bt_deps())) 267 if chk_rt_deps != 0: 268 deps.extend(filter(lambda d: d not in deps, info.get_rt_deps())) 269 270 # Perform validation 271 nerrs = 0 272 nwarns = 0 273 if warn_as_err == 0: 274 warn_pref = WARN_PREF 275 else: 276 warn_pref = ERR_PREF 277 err_pref = ERR_PREF 278 for dep in deps: 279 pi = getpi(dep) 280 if pi.PKGORIGIN not in origins.values(): 281 print formatmsg(seclindent = 7 * 0, msg = \ 282 '%s package %s (%s) belongs to dependency chain, but ' \ 283 'isn\'t installed.' % (err_pref, pi.PKGNAME, pi.PKGORIGIN)) 284 nerrs += 1 285 elif pi.PKGNAME not in origins.keys(): 286 for instpkg in origins.keys(): 287 if origins[instpkg] == pi.PKGORIGIN: 288 break 289 print formatmsg(seclindent = 9 * 0, msg = \ 290 '%s package %s (%s) belongs to dependency chain, but ' \ 291 'package %s is installed instead. Perhaps it\'s an older ' \ 292 'version - update is highly recommended.' % (warn_pref, \ 293 pi.PKGNAME, pi.PKGORIGIN, instpkg)) 294 nwarns += 1 295 296 # Save PortInfo cache if requested 297 if cachefile != None: 298 try: 299 pickle.dump(picache, open(cachefile, 'w')) 300 except: 301 pass 302 303 if warn_as_err != 0: 304 nerrs += nwarns 305 306 return nerrs 307 308 309PORTSDIR, PKG_DBDIR = querymakevars(ROOT_PORTMK, 'PORTSDIR', 'PKG_DBDIR') 310 311if __name__ == '__main__': 312 try: 313 sys.exit(main()) 314 except KeyboardInterrupt: 315 pass 316