1187767Sluigi#!/usr/bin/env python
2187767Sluigi
3187767Sluigifrom __future__ import print_function
4187767Sluigi
5187767Sluigi"""
6187767SluigiThis script parses each "meta" file and extracts the
7187767Sluigiinformation needed to deduce build and src dependencies.
8187767Sluigi
9187767SluigiIt works much the same as the original shell script, but is
10187767Sluigi*much* more efficient.
11187767Sluigi
12187767SluigiThe parsing work is handled by the class MetaFile.
13187767SluigiWe only pay attention to a subset of the information in the
14187767Sluigi"meta" files.  Specifically:
15187767Sluigi
16187767Sluigi'CWD'	to initialize our notion.
17187767Sluigi
18187767Sluigi'C'	to track chdir(2) on a per process basis
19187767Sluigi
20187767Sluigi'R'	files read are what we really care about.
21187767Sluigi	directories read, provide a clue to resolving
22187767Sluigi	subsequent relative paths.  That is if we cannot find
23187767Sluigi	them relative to 'cwd', we check relative to the last
24187767Sluigi	dir read.
25187767Sluigi
26187767Sluigi'W'	files opened for write or read-write,
27187767Sluigi	for filemon V3 and earlier.
28187767Sluigi
29187767Sluigi'E'	files executed.
30187767Sluigi
31187767Sluigi'L'	files linked
32187767Sluigi
33187767Sluigi'V'	the filemon version, this record is used as a clue
34187767Sluigi	that we have reached the interesting bit.
35187767Sluigi
36187767Sluigi"""
37187767Sluigi
38187767Sluigi"""
39187767SluigiSPDX-License-Identifier: BSD-2-Clause
40187767Sluigi
41187767SluigiRCSid:
42187767Sluigi	$Id: meta2deps.py,v 1.47 2024/02/17 17:26:57 sjg Exp $
43187767Sluigi
44187767Sluigi	Copyright (c) 2011-2020, Simon J. Gerraty
45187767Sluigi	Copyright (c) 2011-2017, Juniper Networks, Inc.
46187767Sluigi	All rights reserved.
47187767Sluigi
48187767Sluigi	Redistribution and use in source and binary forms, with or without
49187767Sluigi	modification, are permitted provided that the following conditions
50187767Sluigi	are met:
51187767Sluigi	1. Redistributions of source code must retain the above copyright
52187767Sluigi	   notice, this list of conditions and the following disclaimer.
53187767Sluigi	2. Redistributions in binary form must reproduce the above copyright
54187767Sluigi	   notice, this list of conditions and the following disclaimer in the
55187767Sluigi	   documentation and/or other materials provided with the distribution.
56187767Sluigi
57187767Sluigi	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
58187767Sluigi	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
59187767Sluigi	LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
60187767Sluigi	A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
61187767Sluigi	OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
62187767Sluigi	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
63187767Sluigi	LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
64187767Sluigi	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
65187767Sluigi	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
66187767Sluigi	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
67187767Sluigi	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
68187767Sluigi
69187767Sluigi"""
70187767Sluigi
71187767Sluigiimport os
72187767Sluigiimport re
73187767Sluigiimport sys
74187769Sluigiimport stat
75187769Sluigi
76187769Sluigidef resolve(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
77187769Sluigi    """
78187769Sluigi    Return an absolute path, resolving via cwd or last_dir if needed.
79187769Sluigi
80187769Sluigi    Cleanup any leading ``./`` and trailing ``/.``
81187769Sluigi    """
82187769Sluigi    while path.endswith('/.'):
83187769Sluigi        path = path[0:-2]
84187769Sluigi    if len(path) > 0 and path[0] == '/':
85187769Sluigi        if os.path.exists(path):
86187769Sluigi            return path
87187769Sluigi        if debug > 2:
88187769Sluigi            print("skipping non-existent:", path, file=debug_out)
89187769Sluigi        return None
90187769Sluigi    if path == '.':
91187769Sluigi        return cwd
92187769Sluigi    if path.startswith('./'):
93187769Sluigi        while path.startswith('./'):
94187769Sluigi            path = path[1:]
95187769Sluigi        return cwd + path
96187769Sluigi    if last_dir == cwd:
97187769Sluigi        last_dir = None
98187769Sluigi    for d in [last_dir, cwd]:
99187769Sluigi        if not d:
100187769Sluigi            continue
101187769Sluigi        if path == '..':
102187769Sluigi            dw = d.split('/')
103187769Sluigi            p = '/'.join(dw[:-1])
104187769Sluigi            if not p:
105187769Sluigi                p = '/'
106187769Sluigi            return p
107187769Sluigi        p = '/'.join([d,path])
108187769Sluigi        if debug > 2:
109187769Sluigi            print("looking for:", p, end=' ', file=debug_out)
110187769Sluigi        if not os.path.exists(p):
111187769Sluigi            if debug > 2:
112187769Sluigi                print("nope", file=debug_out)
113187769Sluigi            p = None
114187769Sluigi            continue
115187769Sluigi        if debug > 2:
116187769Sluigi            print("found:", p, file=debug_out)
117187769Sluigi        return p
118187769Sluigi    return None
119187769Sluigi
120187769Sluigidef cleanpath(path):
121187769Sluigi    """cleanup path without using realpath(3)"""
122187769Sluigi    if path.startswith('/'):
123187769Sluigi        r = '/'
124187769Sluigi    else:
125187769Sluigi        r = ''
126187769Sluigi    p = []
127187769Sluigi    w = path.split('/')
128187769Sluigi    for d in w:
129187769Sluigi        if not d or d == '.':
130187769Sluigi            continue
131187769Sluigi        if d == '..':
132187769Sluigi            try:
133187769Sluigi                p.pop()
134187769Sluigi                continue
135187769Sluigi            except:
136187769Sluigi                break
137187769Sluigi        p.append(d)
138187769Sluigi
139187769Sluigi    return r + '/'.join(p)
140187769Sluigi
141187769Sluigidef abspath(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
142187769Sluigi    """
143187769Sluigi    Return an absolute path, resolving via cwd or last_dir if needed.
144187769Sluigi    this gets called a lot, so we try to avoid calling realpath.
145187769Sluigi    """
146187769Sluigi    rpath = resolve(path, cwd, last_dir, debug, debug_out)
147187769Sluigi    if rpath:
148187769Sluigi        path = rpath
149187769Sluigi    elif len(path) > 0 and path[0] == '/':
150187769Sluigi        return None
151187769Sluigi    if (path.find('/') < 0 or
152187769Sluigi        path.find('./') > 0 or
153187769Sluigi        path.find('/../') > 0 or
154187769Sluigi        path.endswith('/..')):
155187769Sluigi        path = cleanpath(path)
156187769Sluigi    return path
157187769Sluigi
158187769Sluigidef sort_unique(list, cmp=None, key=None, reverse=False):
159187769Sluigi    if sys.version_info[0] == 2:
160187769Sluigi        list.sort(cmp, key, reverse)
161187769Sluigi    else:
162187769Sluigi        list.sort(reverse=reverse)
163187769Sluigi    nl = []
164187769Sluigi    le = None
165187769Sluigi    for e in list:
166187769Sluigi        if e == le:
167187769Sluigi            continue
168187769Sluigi        le = e
169187769Sluigi        nl.append(e)
170187769Sluigi    return nl
171187769Sluigi
172187769Sluigidef add_trims(x):
173187769Sluigi    return ['/' + x + '/',
174187769Sluigi            '/' + x,
175187769Sluigi            x + '/',
176187769Sluigi            x]
177187769Sluigi
178187769Sluigidef target_spec_exts(target_spec):
179187769Sluigi    """return a list of dirdep extensions that could match target_spec"""
180187769Sluigi
181187769Sluigi    if target_spec.find(',') < 0:
182187769Sluigi        return ['.'+target_spec]
183187769Sluigi    w = target_spec.split(',')
184187769Sluigi    n = len(w)
185187769Sluigi    e = []
186187769Sluigi    while n > 0:
187187767Sluigi        e.append('.'+','.join(w[0:n]))
188187767Sluigi        n -= 1
189187767Sluigi    return e
190187767Sluigi
191187767Sluigiclass MetaFile:
192187767Sluigi    """class to parse meta files generated by bmake."""
193187787Sluigi
194187787Sluigi    conf = None
195187767Sluigi    dirdep_re = None
196187767Sluigi    host_target = None
197187767Sluigi    srctops = []
198187767Sluigi    objroots = []
199187770Sluigi    excludes = []
200187767Sluigi    seen = {}
201187769Sluigi    obj_deps = []
202187767Sluigi    src_deps = []
203187770Sluigi    file_deps = []
204187769Sluigi
205187770Sluigi    def __init__(self, name, conf={}):
206187770Sluigi        """if name is set we will parse it now.
207187769Sluigi        conf can have the follwing keys:
208187769Sluigi
209187769Sluigi        SRCTOPS list of tops of the src tree(s).
210187769Sluigi
211187770Sluigi        CURDIR  the src directory 'bmake' was run from.
212187769Sluigi
213187819Sluigi        RELDIR  the relative path from SRCTOP to CURDIR
214187819Sluigi
215187819Sluigi        MACHINE the machine we built for.
216187819Sluigi                set to 'none' if we are not cross-building.
217187819Sluigi                More specifically if machine cannot be deduced from objdirs.
218187819Sluigi
219187819Sluigi        TARGET_SPEC
220187819Sluigi                Sometimes MACHINE isn't enough.
221187819Sluigi
222187983Sluigi        HOST_TARGET
223187819Sluigi                when we build for the pseudo machine 'host'
224187819Sluigi                the object tree uses HOST_TARGET rather than MACHINE.
225187819Sluigi
226187769Sluigi        OBJROOTS a list of the common prefix for all obj dirs it might
227187767Sluigi                end in '/' or '-'.
228187767Sluigi
229187767Sluigi        DPDEPS  names an optional file to which per file dependencies
230187767Sluigi                will be appended.
231187767Sluigi                For example if 'some/path/foo.h' is read from SRCTOP
232187767Sluigi                then 'DPDEPS_some/path/foo.h +=' "RELDIR" is output.
233187767Sluigi                This can allow 'bmake' to learn all the dirs within
234187770Sluigi                the tree that depend on 'foo.h'
235187767Sluigi
236187767Sluigi        EXCLUDES
237187767Sluigi                A list of paths to ignore.
238187767Sluigi                ccache(1) can otherwise be trouble.
239187767Sluigi
240187767Sluigi        debug   desired debug level
241187767Sluigi
242187767Sluigi        debug_out open file to send debug output to (sys.stderr)
243187767Sluigi
244187767Sluigi        """
245187767Sluigi
246187767Sluigi        self.name = name
247187983Sluigi        self.debug = conf.get('debug', 0)
248187983Sluigi        self.debug_out = conf.get('debug_out', sys.stderr)
249187983Sluigi
250187983Sluigi        self.machine = conf.get('MACHINE', '')
251187983Sluigi        self.machine_arch = conf.get('MACHINE_ARCH', '')
252187983Sluigi        self.target_spec = conf.get('TARGET_SPEC', self.machine)
253187770Sluigi        self.exts = target_spec_exts(self.target_spec)
254187769Sluigi        self.curdir = conf.get('CURDIR')
255187769Sluigi        self.reldir = conf.get('RELDIR')
256187769Sluigi        self.dpdeps = conf.get('DPDEPS')
257187770Sluigi        self.pids = {}
258187770Sluigi        self.line = 0
259187819Sluigi
260187819Sluigi        if not self.conf:
261187819Sluigi            # some of the steps below we want to do only once
262187819Sluigi            self.conf = conf
263187770Sluigi            self.host_target = conf.get('HOST_TARGET')
264187819Sluigi            for srctop in conf.get('SRCTOPS', []):
265187819Sluigi                if srctop[-1] != '/':
266187770Sluigi                    srctop += '/'
267187819Sluigi                if not srctop in self.srctops:
268187770Sluigi                    self.srctops.append(srctop)
269187819Sluigi                _srctop = os.path.realpath(srctop)
270187819Sluigi                if _srctop[-1] != '/':
271                    _srctop += '/'
272                if not _srctop in self.srctops:
273                    self.srctops.append(_srctop)
274
275            trim_list = add_trims(self.machine)
276            if self.machine == 'host':
277                trim_list += add_trims(self.host_target)
278            if self.target_spec != self.machine:
279                trim_list += add_trims(self.target_spec)
280
281            for objroot in conf.get('OBJROOTS', []):
282                for e in trim_list:
283                    if objroot.endswith(e):
284                        # this is not what we want - fix it
285                        objroot = objroot[0:-len(e)]
286
287                if objroot[-1] != '/':
288                    objroot += '/'
289                if not objroot in self.objroots:
290                    self.objroots.append(objroot)
291                    _objroot = os.path.realpath(objroot)
292                    if objroot[-1] == '/':
293                        _objroot += '/'
294                    if not _objroot in self.objroots:
295                        self.objroots.append(_objroot)
296
297            # we want the longest match
298            self.srctops.sort(reverse=True)
299            self.objroots.sort(reverse=True)
300
301            self.excludes = conf.get('EXCLUDES', [])
302
303            if self.debug:
304                print("host_target=", self.host_target, file=self.debug_out)
305                print("srctops=", self.srctops, file=self.debug_out)
306                print("objroots=", self.objroots, file=self.debug_out)
307                print("excludes=", self.excludes, file=self.debug_out)
308                print("ext_list=", self.exts, file=self.debug_out)
309
310            self.dirdep_re = re.compile(r'([^/]+)/(.+)')
311
312        if self.dpdeps and not self.reldir:
313            if self.debug:
314                print("need reldir:", end=' ', file=self.debug_out)
315            if self.curdir:
316                srctop = self.find_top(self.curdir, self.srctops)
317                if srctop:
318                    self.reldir = self.curdir.replace(srctop,'')
319                    if self.debug:
320                        print(self.reldir, file=self.debug_out)
321            if not self.reldir:
322                self.dpdeps = None      # we cannot do it?
323
324        self.cwd = os.getcwd()          # make sure this is initialized
325        self.last_dir = self.cwd
326
327        if name:
328            self.try_parse()
329
330    def reset(self):
331        """reset state if we are being passed meta files from multiple directories."""
332        self.seen = {}
333        self.obj_deps = []
334        self.src_deps = []
335        self.file_deps = []
336
337    def dirdeps(self, sep='\n'):
338        """return DIRDEPS"""
339        return sep.strip() + sep.join(self.obj_deps)
340
341    def src_dirdeps(self, sep='\n'):
342        """return SRC_DIRDEPS"""
343        return sep.strip() + sep.join(self.src_deps)
344
345    def file_depends(self, out=None):
346        """Append DPDEPS_${file} += ${RELDIR}
347        for each file we saw, to the output file."""
348        if not self.reldir:
349            return None
350        for f in sort_unique(self.file_deps):
351            print('DPDEPS_%s += %s' % (f, self.reldir), file=out)
352        # these entries provide for reverse DIRDEPS lookup
353        for f in self.obj_deps:
354            print('DEPDIRS_%s += %s' % (f, self.reldir), file=out)
355
356    def seenit(self, dir):
357        """rememer that we have seen dir."""
358        self.seen[dir] = 1
359
360    def add(self, list, data, clue=''):
361        """add data to list if it isn't already there."""
362        if data not in list:
363            list.append(data)
364            if self.debug:
365                print("%s: %sAdd: %s" % (self.name, clue, data), file=self.debug_out)
366
367    def find_top(self, path, list):
368        """the logical tree may be split across multiple trees"""
369        for top in list:
370            if path.startswith(top):
371                if self.debug > 2:
372                    print("found in", top, file=self.debug_out)
373                return top
374        return None
375
376    def find_obj(self, objroot, dir, path, input):
377        """return path within objroot, taking care of .dirdep files"""
378        ddep = None
379        for ddepf in [path + '.dirdep', dir + '/.dirdep']:
380            if not ddep and os.path.exists(ddepf):
381                ddep = open(ddepf, 'r').readline().strip('# \n')
382                if self.debug > 1:
383                    print("found %s: %s\n" % (ddepf, ddep), file=self.debug_out)
384                for e in self.exts:
385                    if ddep.endswith(e):
386                        ddep = ddep[0:-len(e)]
387                        break
388
389        if not ddep:
390            # no .dirdeps, so remember that we've seen the raw input
391            self.seenit(input)
392            self.seenit(dir)
393            if self.machine == 'none':
394                if dir.startswith(objroot):
395                    return dir.replace(objroot,'')
396                return None
397            m = self.dirdep_re.match(dir.replace(objroot,''))
398            if m:
399                ddep = m.group(2)
400                dmachine = m.group(1)
401                if dmachine != self.machine:
402                    if not (self.machine == 'host' and
403                            dmachine == self.host_target):
404                        if self.debug > 2:
405                            print("adding .%s to %s" % (dmachine, ddep), file=self.debug_out)
406                        ddep += '.' + dmachine
407
408        return ddep
409
410    def try_parse(self, name=None, file=None):
411        """give file and line number causing exception"""
412        try:
413            self.parse(name, file)
414        except:
415            # give a useful clue
416            print('{}:{}: '.format(self.name, self.line), end=' ', file=sys.stderr)
417            raise
418
419    def parse(self, name=None, file=None):
420        """A meta file looks like:
421
422        # Meta data file "path"
423        CMD "command-line"
424        CWD "cwd"
425        TARGET "target"
426        -- command output --
427        -- filemon acquired metadata --
428        # buildmon version 3
429        V 3
430        C "pid" "cwd"
431        E "pid" "path"
432        F "pid" "child"
433        R "pid" "path"
434        W "pid" "path"
435        X "pid" "status"
436        D "pid" "path"
437        L "pid" "src" "target"
438        M "pid" "old" "new"
439        S "pid" "path"
440        # Bye bye
441
442        We go to some effort to avoid processing a dependency more than once.
443        Of the above record types only C,E,F,L,R,V and W are of interest.
444        """
445
446        version = 0                     # unknown
447        if name:
448            self.name = name;
449        if file:
450            f = file
451            cwd = self.last_dir = self.cwd
452        else:
453            f = open(self.name, 'r')
454        skip = True
455        pid_cwd = {}
456        pid_last_dir = {}
457        last_pid = 0
458        eof_token = False
459
460        self.line = 0
461        if self.curdir:
462            self.seenit(self.curdir)    # we ignore this
463
464        interesting = '#CEFLRVX'
465        for line in f:
466            self.line += 1
467            # ignore anything we don't care about
468            if not line[0] in interesting:
469                continue
470            if self.debug > 2:
471                print("input:", line, end=' ', file=self.debug_out)
472            w = line.split()
473
474            if skip:
475                if w[0] == 'V':
476                    skip = False
477                    version = int(w[1])
478                    """
479                    if version < 4:
480                        # we cannot ignore 'W' records
481                        # as they may be 'rw'
482                        interesting += 'W'
483                    """
484                elif w[0] == 'CWD':
485                    self.cwd = cwd = self.last_dir = w[1]
486                    self.seenit(cwd)    # ignore this
487                    if self.debug:
488                        print("%s: CWD=%s" % (self.name, cwd), file=self.debug_out)
489                continue
490
491            if w[0] == '#':
492                # check the file has not been truncated
493                if line.find('Bye') > 0:
494                    eof_token = True
495                continue
496
497            pid = int(w[1])
498            if pid != last_pid:
499                if last_pid:
500                    pid_last_dir[last_pid] = self.last_dir
501                cwd = pid_cwd.get(pid, self.cwd)
502                self.last_dir = pid_last_dir.get(pid, self.cwd)
503                last_pid = pid
504
505            # process operations
506            if w[0] == 'F':
507                npid = int(w[2])
508                pid_cwd[npid] = cwd
509                pid_last_dir[npid] = cwd
510                last_pid = npid
511                continue
512            elif w[0] == 'C':
513                cwd = abspath(w[2], cwd, None, self.debug, self.debug_out)
514                if not cwd:
515                    cwd = w[2]
516                    if self.debug > 1:
517                        print("missing cwd=", cwd, file=self.debug_out)
518                if cwd.endswith('/.'):
519                    cwd = cwd[0:-2]
520                self.last_dir = pid_last_dir[pid] = cwd
521                pid_cwd[pid] = cwd
522                if self.debug > 1:
523                    print("cwd=", cwd, file=self.debug_out)
524                continue
525
526            if w[0] == 'X':
527                try:
528                    del self.pids[pid]
529                except KeyError:
530                    pass
531                continue
532
533            if w[2] in self.seen:
534                if self.debug > 2:
535                    print("seen:", w[2], file=self.debug_out)
536                continue
537            # file operations
538            if w[0] in 'ML':
539                # these are special, tread src as read and
540                # target as write
541                self.parse_path(w[2].strip("'"), cwd, 'R', w)
542                self.parse_path(w[3].strip("'"), cwd, 'W', w)
543                continue
544            elif w[0] in 'ERWS':
545                path = w[2]
546                if w[0] == 'E':
547                    self.pids[pid] = path
548                elif path == '.':
549                    continue
550                self.parse_path(path, cwd, w[0], w)
551
552        if version == 0:
553            raise AssertionError('missing filemon data')
554        if not eof_token:
555            raise AssertionError('truncated filemon data')
556
557        setid_pids = []
558        # self.pids should be empty!
559        for pid,path in self.pids.items():
560            try:
561                # no guarantee that path is still valid
562                if os.stat(path).st_mode & (stat.S_ISUID|stat.S_ISGID):
563                    # we do not expect anything after Exec
564                    setid_pids.append(pid)
565                    continue
566            except:
567                # we do not care why the above fails,
568                # we do not want to miss the ERROR below.
569                pass
570            print("ERROR: missing eXit for {} pid {}".format(path, pid))
571        for pid in setid_pids:
572            del self.pids[pid]
573        assert(len(self.pids) == 0)
574        if not file:
575            f.close()
576
577    def is_src(self, base, dir, rdir):
578        """is base in srctop"""
579        for dir in [dir,rdir]:
580            if not dir:
581                continue
582            path = '/'.join([dir,base])
583            srctop = self.find_top(path, self.srctops)
584            if srctop:
585                if self.dpdeps:
586                    self.add(self.file_deps, path.replace(srctop,''), 'file')
587                self.add(self.src_deps, dir.replace(srctop,''), 'src')
588                self.seenit(dir)
589                return True
590        return False
591
592    def parse_path(self, path, cwd, op=None, w=[]):
593        """look at a path for the op specified"""
594
595        if not op:
596            op = w[0]
597
598        # we are never interested in .dirdep files as dependencies
599        if path.endswith('.dirdep'):
600            return
601        for p in self.excludes:
602            if p and path.startswith(p):
603                if self.debug > 2:
604                    print("exclude:", p, path, file=self.debug_out)
605                return
606        # we don't want to resolve the last component if it is
607        # a symlink
608        path = resolve(path, cwd, self.last_dir, self.debug, self.debug_out)
609        if not path:
610            return
611        dir,base = os.path.split(path)
612        if dir in self.seen:
613            if self.debug > 2:
614                print("seen:", dir, file=self.debug_out)
615            return
616        # we can have a path in an objdir which is a link
617        # to the src dir, we may need to add dependencies for each
618        rdir = dir
619        dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out)
620        if dir:
621            rdir = os.path.realpath(dir)
622        else:
623            dir = rdir
624        if rdir == dir:
625            rdir = None
626        # now put path back together
627        path = '/'.join([dir,base])
628        if self.debug > 1:
629            print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out)
630        if op in 'RWS':
631            if path in [self.last_dir, cwd, self.cwd, self.curdir]:
632                if self.debug > 1:
633                    print("skipping:", path, file=self.debug_out)
634                return
635            if os.path.isdir(path):
636                if op in 'RW':
637                    self.last_dir = path;
638                if self.debug > 1:
639                    print("ldir=", self.last_dir, file=self.debug_out)
640                return
641
642        if op in 'ER':
643            # finally, we get down to it
644            if dir == self.cwd or dir == self.curdir:
645                return
646            if self.is_src(base, dir, rdir):
647                self.seenit(w[2])
648                if not rdir:
649                    return
650
651            objroot = None
652            for dir in [dir,rdir]:
653                if not dir:
654                    continue
655                objroot = self.find_top(dir, self.objroots)
656                if objroot:
657                    break
658            if objroot:
659                ddep = self.find_obj(objroot, dir, path, w[2])
660                if ddep:
661                    self.add(self.obj_deps, ddep, 'obj')
662                    if self.dpdeps and objroot.endswith('/stage/'):
663                        sp = '/'.join(path.replace(objroot,'').split('/')[1:])
664                        self.add(self.file_deps, sp, 'file')
665            else:
666                # don't waste time looking again
667                self.seenit(w[2])
668                self.seenit(dir)
669
670
671def main(argv, klass=MetaFile, xopts='', xoptf=None):
672    """Simple driver for class MetaFile.
673
674    Usage:
675        script [options] [key=value ...] "meta" ...
676
677    Options and key=value pairs contribute to the
678    dictionary passed to MetaFile.
679
680    -S "SRCTOP"
681                add "SRCTOP" to the "SRCTOPS" list.
682
683    -C "CURDIR"
684
685    -O "OBJROOT"
686                add "OBJROOT" to the "OBJROOTS" list.
687
688    -m "MACHINE"
689
690    -a "MACHINE_ARCH"
691
692    -H "HOST_TARGET"
693
694    -D "DPDEPS"
695
696    -d  bumps debug level
697
698    """
699    import getopt
700
701    # import Psyco if we can
702    # it can speed things up quite a bit
703    have_psyco = 0
704    try:
705        import psyco
706        psyco.full()
707        have_psyco = 1
708    except:
709        pass
710
711    conf = {
712        'SRCTOPS': [],
713        'OBJROOTS': [],
714        'EXCLUDES': [],
715        }
716
717    try:
718        machine = os.environ['MACHINE']
719        if machine:
720            conf['MACHINE'] = machine
721        machine_arch = os.environ['MACHINE_ARCH']
722        if machine_arch:
723            conf['MACHINE_ARCH'] = machine_arch
724        srctop = os.environ['SB_SRC']
725        if srctop:
726            conf['SRCTOPS'].append(srctop)
727        objroot = os.environ['SB_OBJROOT']
728        if objroot:
729            conf['OBJROOTS'].append(objroot)
730    except:
731        pass
732
733    debug = 0
734    output = True
735
736    opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
737    for o, a in opts:
738        if o == '-a':
739            conf['MACHINE_ARCH'] = a
740        elif o == '-d':
741            debug += 1
742        elif o == '-q':
743            output = False
744        elif o == '-H':
745            conf['HOST_TARGET'] = a
746        elif o == '-S':
747            if a not in conf['SRCTOPS']:
748                conf['SRCTOPS'].append(a)
749        elif o == '-C':
750            conf['CURDIR'] = a
751        elif o == '-O':
752            if a not in conf['OBJROOTS']:
753                conf['OBJROOTS'].append(a)
754        elif o == '-R':
755            conf['RELDIR'] = a
756        elif o == '-D':
757            conf['DPDEPS'] = a
758        elif o == '-m':
759            conf['MACHINE'] = a
760        elif o == '-T':
761            conf['TARGET_SPEC'] = a
762        elif o == '-X':
763            if a not in conf['EXCLUDES']:
764                conf['EXCLUDES'].append(a)
765        elif xoptf:
766            xoptf(o, a, conf)
767
768    conf['debug'] = debug
769
770    # get any var=val assignments
771    eaten = []
772    for a in args:
773        if a.find('=') > 0:
774            k,v = a.split('=')
775            if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']:
776                if k == 'SRCTOP':
777                    k = 'SRCTOPS'
778                elif k == 'OBJROOT':
779                    k = 'OBJROOTS'
780                if v not in conf[k]:
781                    conf[k].append(v)
782            else:
783                conf[k] = v
784            eaten.append(a)
785            continue
786        break
787
788    for a in eaten:
789        args.remove(a)
790
791    debug_out = conf.get('debug_out', sys.stderr)
792
793    if debug:
794        print("config:", file=debug_out)
795        print("psyco=", have_psyco, file=debug_out)
796        for k,v in list(conf.items()):
797            print("%s=%s" % (k,v), file=debug_out)
798
799    m = None
800    for a in args:
801        if a.endswith('.meta'):
802            if not os.path.exists(a):
803                continue
804            m = klass(a, conf)
805        elif a.startswith('@'):
806            # there can actually multiple files per line
807            for line in open(a[1:]):
808                for f in line.strip().split():
809                    if not os.path.exists(f):
810                        continue
811                    m = klass(f, conf)
812
813    if output and m:
814        print(m.dirdeps())
815
816        print(m.src_dirdeps('\nsrc:'))
817
818        dpdeps = conf.get('DPDEPS')
819        if dpdeps:
820            m.file_depends(open(dpdeps, 'w'))
821
822    return m
823
824if __name__ == '__main__':
825    try:
826        main(sys.argv)
827    except:
828        # yes, this goes to stdout
829        print("ERROR: ", sys.exc_info()[1])
830        raise
831
832