xref: /freebsd/share/mk/meta2deps.py (revision 8d13bc63c0e1d50bc9e47ac1f26329c999bfecf0)
1#!/usr/bin/env python
2
3from __future__ import print_function
4
5"""
6This script parses each "meta" file and extracts the
7information needed to deduce build and src dependencies.
8
9It works much the same as the original shell script, but is
10*much* more efficient.
11
12The parsing work is handled by the class MetaFile.
13We only pay attention to a subset of the information in the
14"meta" files.  Specifically:
15
16'CWD'	to initialize our notion.
17
18'C'	to track chdir(2) on a per process basis
19
20'R'	files read are what we really care about.
21	directories read, provide a clue to resolving
22	subsequent relative paths.  That is if we cannot find
23	them relative to 'cwd', we check relative to the last
24	dir read.
25
26'W'	files opened for write or read-write,
27	for filemon V3 and earlier.
28
29'E'	files executed.
30
31'L'	files linked
32
33'V'	the filemon version, this record is used as a clue
34	that we have reached the interesting bit.
35
36"""
37
38"""
39SPDX-License-Identifier: BSD-2-Clause
40
41RCSid:
42	$Id: meta2deps.py,v 1.47 2024/02/17 17:26:57 sjg Exp $
43
44	Copyright (c) 2011-2020, Simon J. Gerraty
45	Copyright (c) 2011-2017, Juniper Networks, Inc.
46	All rights reserved.
47
48	Redistribution and use in source and binary forms, with or without
49	modification, are permitted provided that the following conditions
50	are met:
51	1. Redistributions of source code must retain the above copyright
52	   notice, this list of conditions and the following disclaimer.
53	2. Redistributions in binary form must reproduce the above copyright
54	   notice, this list of conditions and the following disclaimer in the
55	   documentation and/or other materials provided with the distribution.
56
57	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
58	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
59	LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
60	A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
61	OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
62	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
63	LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
64	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
65	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
66	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
67	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
68
69"""
70
71import os
72import re
73import sys
74import stat
75
76def resolve(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
77    """
78    Return an absolute path, resolving via cwd or last_dir if needed.
79
80    Cleanup any leading ``./`` and trailing ``/.``
81    """
82    while path.endswith('/.'):
83        path = path[0:-2]
84    if len(path) > 0 and path[0] == '/':
85        if os.path.exists(path):
86            return path
87        if debug > 2:
88            print("skipping non-existent:", path, file=debug_out)
89        return None
90    if path == '.':
91        return cwd
92    if path.startswith('./'):
93        while path.startswith('./'):
94            path = path[1:]
95        return cwd + path
96    if last_dir == cwd:
97        last_dir = None
98    for d in [last_dir, cwd]:
99        if not d:
100            continue
101        if path == '..':
102            dw = d.split('/')
103            p = '/'.join(dw[:-1])
104            if not p:
105                p = '/'
106            return p
107        p = '/'.join([d,path])
108        if debug > 2:
109            print("looking for:", p, end=' ', file=debug_out)
110        if not os.path.exists(p):
111            if debug > 2:
112                print("nope", file=debug_out)
113            p = None
114            continue
115        if debug > 2:
116            print("found:", p, file=debug_out)
117        return p
118    return None
119
120def cleanpath(path):
121    """cleanup path without using realpath(3)"""
122    if path.startswith('/'):
123        r = '/'
124    else:
125        r = ''
126    p = []
127    w = path.split('/')
128    for d in w:
129        if not d or d == '.':
130            continue
131        if d == '..':
132            try:
133                p.pop()
134                continue
135            except:
136                break
137        p.append(d)
138
139    return r + '/'.join(p)
140
141def abspath(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
142    """
143    Return an absolute path, resolving via cwd or last_dir if needed.
144    this gets called a lot, so we try to avoid calling realpath.
145    """
146    rpath = resolve(path, cwd, last_dir, debug, debug_out)
147    if rpath:
148        path = rpath
149    elif len(path) > 0 and path[0] == '/':
150        return None
151    if (path.find('/') < 0 or
152        path.find('./') > 0 or
153        path.find('/../') > 0 or
154        path.endswith('/..')):
155        path = cleanpath(path)
156    return path
157
158def sort_unique(list, cmp=None, key=None, reverse=False):
159    if sys.version_info[0] == 2:
160        list.sort(cmp, key, reverse)
161    else:
162        list.sort(reverse=reverse)
163    nl = []
164    le = None
165    for e in list:
166        if e == le:
167            continue
168        le = e
169        nl.append(e)
170    return nl
171
172def add_trims(x):
173    return ['/' + x + '/',
174            '/' + x,
175            x + '/',
176            x]
177
178def target_spec_exts(target_spec):
179    """return a list of dirdep extensions that could match target_spec"""
180
181    if target_spec.find(',') < 0:
182        return ['.'+target_spec]
183    w = target_spec.split(',')
184    n = len(w)
185    e = []
186    while n > 0:
187        e.append('.'+','.join(w[0:n]))
188        n -= 1
189    return e
190
191class MetaFile:
192    """class to parse meta files generated by bmake."""
193
194    conf = None
195    dirdep_re = None
196    host_target = None
197    srctops = []
198    objroots = []
199    excludes = []
200    seen = {}
201    obj_deps = []
202    src_deps = []
203    file_deps = []
204
205    def __init__(self, name, conf={}):
206        """if name is set we will parse it now.
207        conf can have the follwing keys:
208
209        SRCTOPS list of tops of the src tree(s).
210
211        CURDIR  the src directory 'bmake' was run from.
212
213        RELDIR  the relative path from SRCTOP to CURDIR
214
215        MACHINE the machine we built for.
216                set to 'none' if we are not cross-building.
217                More specifically if machine cannot be deduced from objdirs.
218
219        TARGET_SPEC
220                Sometimes MACHINE isn't enough.
221
222        HOST_TARGET
223                when we build for the pseudo machine 'host'
224                the object tree uses HOST_TARGET rather than MACHINE.
225
226        OBJROOTS a list of the common prefix for all obj dirs it might
227                end in '/' or '-'.
228
229        DPDEPS  names an optional file to which per file dependencies
230                will be appended.
231                For example if 'some/path/foo.h' is read from SRCTOP
232                then 'DPDEPS_some/path/foo.h +=' "RELDIR" is output.
233                This can allow 'bmake' to learn all the dirs within
234                the tree that depend on 'foo.h'
235
236        EXCLUDES
237                A list of paths to ignore.
238                ccache(1) can otherwise be trouble.
239
240        debug   desired debug level
241
242        debug_out open file to send debug output to (sys.stderr)
243
244        """
245
246        self.name = name
247        self.debug = conf.get('debug', 0)
248        self.debug_out = conf.get('debug_out', sys.stderr)
249
250        self.machine = conf.get('MACHINE', '')
251        self.machine_arch = conf.get('MACHINE_ARCH', '')
252        self.target_spec = conf.get('TARGET_SPEC', self.machine)
253        self.exts = target_spec_exts(self.target_spec)
254        self.curdir = conf.get('CURDIR')
255        self.reldir = conf.get('RELDIR')
256        self.dpdeps = conf.get('DPDEPS')
257        self.pids = {}
258        self.line = 0
259
260        if not self.conf:
261            # some of the steps below we want to do only once
262            self.conf = conf
263            self.host_target = conf.get('HOST_TARGET')
264            for srctop in conf.get('SRCTOPS', []):
265                if srctop[-1] != '/':
266                    srctop += '/'
267                if not srctop in self.srctops:
268                    self.srctops.append(srctop)
269                _srctop = os.path.realpath(srctop)
270                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