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