포트를 만들거나 업글할 때 가장 귀찮은 과정 중의 하나인 plist를 자동으로 만드는 방법은 mtree기반으로 된 것이 있었지만, 뭔가 다른 프리픽스로 한 번 깔아보는 게 대략 귀찮은 관계로.. ktrace 를 이용한 방법을 한 번 생각해 봤습니다. 흐흐흐.. open 시스템 콜을 모두 추적해서 O_WRONLY나 O_CREAT로 열린 녀석들 목록을 자동으로 plist로 만들어주는 것인데, 대충 만들어 본 소스는..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/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
흐흐.. 그런데, 대충 만들고 나서 보니, 속도가 너무 느리고 (좀만 대형 포트로 가면 엄청난 양의 kdump가 나와버려서 그것 파싱하는데 십분이 넘게 걸립니다 –;) mtree기반의 녀석에게 특별한 장점이 없다는 결론이.. 흐흐흐… (그리고, 위 구현에서는 디렉토리를 인스톨 도중에 mv 해 버린다던지 하는 것에 대해서 무방비 –;)
그래서, 이 녀석을 좀 변모시켜서 plistlint 정도의 이름으로 인스톨하면서 plist에 없는 프로그램 건드리지는 않나, 빼먹은 파일 없나, /tmp에 이상한 파일 남기지 않나 등등을 검사하는 걸로 만들어 볼 까 합니당. 크크