#!/usr/bin/env python
#
# autoplist.py - Automatic pkg-plist generator based on ktrace
#
# ----------------------------------------------------------------------------
# "THE BEER-WARE LICENSE" (Revision 42, (c) Poul-Henning Kamp):
# Hye-Shik Chang <perky@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.
#
# Hye-Shik Chang
# ----------------------------------------------------------------------------
# $FreeBSD$
import os, random
import sys, re
import time
random.whseed()
TMPNAM = '.autoplist.%d.%x' % (os.getpid(), random.randrange(65536))
KTRACECMD = 'ktrace -f %s -id -t cn %%s' % TMPNAM
KDUMPCMD = 'kdump -f %s' % TMPNAM
O_WFLAGS = os.O_WRONLY | os.O_RDWR | os.O_CREAT
PREFIX = '/usr/local'
IGNOREPREFIX = ['/dev/', '/tmp/']
PLIST_SUBS = (
'PYTHON_INCLUDEDIR', 'PYTHON_LIBDIR', 'PYTHON_SITELIBDIR',
'SITE_PERL', 'SITE_RUBY', 'DOCSDIR', 'EXAMPLESDIR', 'DATADIR',
)
PLISTREPLACE = []
def errorexit(msg):
print >> sys.stdout, "autoplist.py:", msg
sys.exit(-1)
def cleanup_tmpfiles():
try:
os.unlink(TMPNAM)
except:
pass
def getmakeenv(envname):
return os.popen('make -V "%s"' % envname).read().strip()
def echo_msg(msg):
print '===> ', msg
class InstallState:
procs = {}
lastforker = 0
def __init__(self, pid, procname):
self.pid = pid
self.procname = procname
if not self.lastforker:
self.curdir = os.getcwd()
else:
self.curdir = self.procs[self.lastforker].curdir
self.procs[pid] = self
self.files, self.dirs = {}, {}
self.trackatom = {}
def feed(self, args):
if self.trackatom:
if args[2] == 'NAMI':
self.trackatom['nami'].append(eval(args[3]))
elif args[2] == 'RET':
rtok = args[3].split()
if rtok[1].isdigit() or rtok[1].startswith('0x'):
self.trackatom['exit'] = eval(rtok[1])
else:
self.trackatom['exit'] = rtok[1]
if len(rtok) >= 3:
self.trackatom['errno'] = eval(rtok[3])
self.newsyscall(**self.trackatom)
self.trackatom = {}
elif args[2] == 'CALL':
syscall, ignore, args = re_syscallargs.findall(args[3])[0]
self.trackatom['syscall'] = syscall
self.trackatom['args'] = args and args.split(',') or []
self.trackatom['nami'] = []
if syscall in ('fork', 'vfork', 'rfork'):
InstallState.lastforker = self.pid
def getabspath(self, path):
if path.startswith('/'):
return path
else:
return os.path.realpath(os.sep.join([self.curdir, path]))
def newsyscall(self, syscall, args, nami, exit, errno=0):
if syscall == 'open':
if eval(args[1]) & O_WFLAGS:
self.files[self.getabspath(nami[0])] = None
elif syscall == 'mkdir':
self.dirs[self.getabspath(nami[0])] = None
elif syscall == 'chdir' and exit == 0:
self.curdir = self.getabspath(nami[0])
elif syscall in ('execve',) and exit == 0:
self.procname = nami[0]
def filterfilelist(files):
filelist = []
warnlist = []
prefix = PREFIX + os.sep
for f in files:
for pfx in IGNOREPREFIX:
if f.startswith(pfx):
continue
if f.startswith(prefix):
filelist.append(f[len(prefix):])
for var, repl in PLISTREPLACE:
if filelist[-1].startswith(repl):
filelist[-1] = filelist[-1].replace(repl, '%%'+var+'%%')
else:
warnlist.append(f)
return filelist, warnlist
re_syscallargs = re.compile('([A-Za-z0-9_-]+)(\(([^)]*)\))?')
def getwfiles():
entry = {}
istate = InstallState.procs
echo_msg('Analyzing installation syscall logs')
for l in os.popen(KDUMPCMD):
ltok = l[:-1].split(None, 3)
pid = int(ltok[0])
if not istate.has_key(pid):
proc = istate[pid] = InstallState(pid, ltok[1])
else:
proc = istate[pid]
proc.feed(ltok)
echo_msg('Generating auto plist')
fo = open("pkg-plist.autogen", "w")
print >>fo, "@comment Generated by autoplist.py", time.asctime()
files = {}
dirs = {}
for pid, data in istate.iteritems():
files.update(data.files)
dirs.update(data.dirs)
files = files.keys()
files.sort()
files, warnfiles = filterfilelist(files)
for file in files:
print >>fo, file
dirs = dirs.keys()
dirs.sort()
dirs.reverse()
dirs, warndirs = filterfilelist(dirs)
for dir in dirs:
print >>fo, "@dirrm", dir
return istate
def main():
try:
err = os.system('make build')
if err != 0:
errorexit("`make' exited with error code %d" % err)
err = os.system(KTRACECMD % 'make install')
if err != 0:
errorexit("`make' exited with error code %d" % err)
getwfiles()
finally:
cleanup_tmpfiles()
def buildenvironment():
global IGNOREPREFIX, PREFIX, PLISTREPLACE
IGNOREPREFIX.append(os.path.abspath(getmakeenv('WRKDIRPREFIX')))
IGNOREPREFIX.append(os.path.abspath(getmakeenv('DISTDIR')))
IGNOREPREFIX.append(os.path.abspath(getmakeenv('PKG_DBDIR')))
PREFIX = getmakeenv('PREFIX')
for var, raw, stripped in re.findall('([A-Za-z_0-9]+)=("([^"]*)"|[^"]\S*)',
getmakeenv('PLIST_SUB')):
if var in PLIST_SUBS:
PLISTREPLACE.append((var, stripped or raw))
PLISTREPLACE.sort(lambda x,y: -cmp(x[1], y[1]))
if __name__ == '__main__':
buildenvironment()
main()
# ex: ts=9 sts=4 sw=4 et