xref: /freebsd/contrib/bmake/mk/meta2deps.py (revision 4543ef516683042d46f3bd3bb8a4f3f746e00499)
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"""
39RCSid:
40	$Id: meta2deps.py,v 1.45 2023/01/18 01:35:24 sjg Exp $
41
42	Copyright (c) 2011-2020, Simon J. Gerraty
43	Copyright (c) 2011-2017, Juniper Networks, Inc.
44	All rights reserved.
45
46	Redistribution and use in source and binary forms, with or without
47	modification, are permitted provided that the following conditions
48	are met:
49	1. Redistributions of source code must retain the above copyright
50	   notice, this list of conditions and the following disclaimer.
51	2. Redistributions in binary form must reproduce the above copyright
52	   notice, this list of conditions and the following disclaimer in the
53	   documentation and/or other materials provided with the distribution.
54
55	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
56	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
57	LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
58	A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
59	OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
60	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
61	LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
62	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
63	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
64	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
65	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
66
67"""
68
69import os
70import re
71import sys
72import stat
73
74def resolve(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
75    """
76    Return an absolute path, resolving via cwd or last_dir if needed.
77    """
78    if path.endswith('/.'):
79        path = path[0:-2]
80    if len(path) > 0 and path[0] == '/':
81        if os.path.exists(path):
82            return path
83        if debug > 2:
84            print("skipping non-existent:", path, file=debug_out)
85        return None
86    if path == '.':
87        return cwd
88    if path.startswith('./'):
89        return cwd + path[1:]
90    if last_dir == cwd:
91        last_dir = None
92    for d in [last_dir, cwd]:
93        if not d:
94            continue
95        if path == '..':
96            dw = d.split('/')
97            p = '/'.join(dw[:-1])
98            if not p:
99                p = '/'
100            return p
101        p = '/'.join([d,path])
102        if debug > 2:
103            print("looking for:", p, end=' ', file=debug_out)
104        if not os.path.exists(p):
105            if debug > 2:
106                print("nope", file=debug_out)
107            p = None
108            continue
109        if debug > 2:
110            print("found:", p, file=debug_out)
111        return p
112    return None
113
114def cleanpath(path):
115    """cleanup path without using realpath(3)"""
116    if path.startswith('/'):
117        r = '/'
118    else:
119        r = ''
120    p = []
121    w = path.split('/')
122    for d in w:
123        if not d or d == '.':
124            continue
125        if d == '..':
126            try:
127                p.pop()
128                continue
129            except:
130                break
131        p.append(d)
132
133    return r + '/'.join(p)
134
135def abspath(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr):
136    """
137    Return an absolute path, resolving via cwd or last_dir if needed.
138    this gets called a lot, so we try to avoid calling realpath.
139    """
140    rpath = resolve(path, cwd, last_dir, debug, debug_out)
141    if rpath:
142        path = rpath
143    elif len(path) > 0 and path[0] == '/':
144        return None
145    if (path.find('/') < 0 or
146        path.find('./') > 0 or
147        path.endswith('/..')):
148        path = cleanpath(path)
149    return path
150
151def sort_unique(list, cmp=None, key=None, reverse=False):
152    if sys.version_info[0] == 2:
153        list.sort(cmp, key, reverse)
154    else:
155        list.sort(reverse=reverse)
156    nl = []
157    le = None
158    for e in list:
159        if e == le:
160            continue
161        le = e
162        nl.append(e)
163    return nl
164
165def add_trims(x):
166    return ['/' + x + '/',
167            '/' + x,
168            x + '/',
169            x]
170
171def target_spec_exts(target_spec):
172    """return a list of dirdep extensions that could match target_spec"""
173
174    if target_spec.find(',') < 0:
175        return ['.'+target_spec]
176    w = target_spec.split(',')
177    n = len(w)
178    e = []
179    while n > 0:
180        e.append('.'+','.join(w[0:n]))
181        n -= 1
182    return e
183
184class MetaFile:
185    """class to parse meta files generated by bmake."""
186
187    conf = None
188    dirdep_re = None
189    host_target = None
190    srctops = []
191    objroots = []
192    excludes = []
193    seen = {}
194    obj_deps = []
195    src_deps = []
196    file_deps = []
197
198    def __init__(self, name, conf={}):
199        """if name is set we will parse it now.
200        conf can have the follwing keys:
201
202        SRCTOPS list of tops of the src tree(s).
203
204        CURDIR  the src directory 'bmake' was run from.
205
206        RELDIR  the relative path from SRCTOP to CURDIR
207
208        MACHINE the machine we built for.
209                set to 'none' if we are not cross-building.
210                More specifically if machine cannot be deduced from objdirs.
211
212        TARGET_SPEC
213                Sometimes MACHINE isn't enough.
214
215        HOST_TARGET
216                when we build for the pseudo machine 'host'
217                the object tree uses HOST_TARGET rather than MACHINE.
218
219        OBJROOTS a list of the common prefix for all obj dirs it might
220                end in '/' or '-'.
221
222        DPDEPS  names an optional file to which per file dependencies
223                will be appended.
224                For example if 'some/path/foo.h' is read from SRCTOP
225                then 'DPDEPS_some/path/foo.h +=' "RELDIR" is output.
226                This can allow 'bmake' to learn all the dirs within
227                the tree that depend on 'foo.h'
228
229        EXCLUDES
230                A list of paths to ignore.
231                ccache(1) can otherwise be trouble.
232
233        debug   desired debug level
234
235        debug_out open file to send debug output to (sys.stderr)
236
237        """
238
239        self.name = name
240        self.debug = conf.get('debug', 0)
241        self.debug_out = conf.get('debug_out', sys.stderr)
242
243        self.machine = conf.get('MACHINE', '')
244        self.machine_arch = conf.get('MACHINE_ARCH', '')
245        self.target_spec = conf.get('TARGET_SPEC', self.machine)
246        self.exts = target_spec_exts(self.target_spec)
247        self.curdir = conf.get('CURDIR')
248        self.reldir = conf.get('RELDIR')
249        self.dpdeps = conf.get('DPDEPS')
250        self.pids = {}
251        self.line = 0
252
253        if not self.conf:
254            # some of the steps below we want to do only once
255            self.conf = conf
256            self.host_target = conf.get('HOST_TARGET')
257            for srctop in conf.get('SRCTOPS', []):
258                if srctop[-1] != '/':
259                    srctop += '/'
260                if not srctop in self.srctops:
261                    self.srctops.append(srctop)
262                _srctop = os.path.realpath(srctop)
263                if _srctop[-1] != '/':
264                    _srctop += '/'
265                if not _srctop in self.srctops:
266                    self.srctops.append(_srctop)
267
268            trim_list = add_trims(self.machine)
269            if self.machine == 'host':
270                trim_list += add_trims(self.host_target)
271            if self.target_spec != self.machine:
272                trim_list += add_trims(self.target_spec)
273
274            for objroot in conf.get('OBJROOTS', []):
275                for e in trim_list:
276                    if objroot.endswith(e):
277                        # this is not what we want - fix it
278                        objroot = objroot[0:-len(e)]
279
280                if objroot[-1] != '/':
281                    objroot += '/'
282                if not objroot in self.objroots:
283                    self.objroots.append(objroot)
284                    _objroot = os.path.realpath(objroot)
285                    if objroot[-1] == '/':
286                        _objroot += '/'
287                    if not _objroot in self.objroots:
288                        self.objroots.append(_objroot)
289
290            # we want the longest match
291            self.srctops.sort(reverse=True)
292            self.objroots.sort(reverse=True)
293
294            self.excludes = conf.get('EXCLUDES', [])
295
296            if self.debug:
297                print("host_target=", self.host_target, file=self.debug_out)
298                print("srctops=", self.srctops, file=self.debug_out)
299                print("objroots=", self.objroots, file=self.debug_out)
300                print("excludes=", self.excludes, file=self.debug_out)
301                print("ext_list=", self.exts, file=self.debug_out)
302
303            self.dirdep_re = re.compile(r'([^/]+)/(.+)')
304
305        if self.dpdeps and not self.reldir:
306            if self.debug:
307                print("need reldir:", end=' ', file=self.debug_out)
308            if self.curdir:
309                srctop = self.find_top(self.curdir, self.srctops)
310                if srctop:
311                    self.reldir = self.curdir.replace(srctop,'')
312                    if self.debug:
313                        print(self.reldir, file=self.debug_out)
314            if not self.reldir:
315                self.dpdeps = None      # we cannot do it?
316
317        self.cwd = os.getcwd()          # make sure this is initialized
318        self.last_dir = self.cwd
319
320        if name:
321            self.try_parse()
322
323    def reset(self):
324        """reset state if we are being passed meta files from multiple directories."""
325        self.seen = {}
326        self.obj_deps = []
327        self.src_deps = []
328        self.file_deps = []
329
330    def dirdeps(self, sep='\n'):
331        """return DIRDEPS"""
332        return sep.strip() + sep.join(self.obj_deps)
333
334    def src_dirdeps(self, sep='\n'):
335        """return SRC_DIRDEPS"""
336        return sep.strip() + sep.join(self.src_deps)
337
338    def file_depends(self, out=None):
339        """Append DPDEPS_${file} += ${RELDIR}
340        for each file we saw, to the output file."""
341        if not self.reldir:
342            return None
343        for f in sort_unique(self.file_deps):
344            print('DPDEPS_%s += %s' % (f, self.reldir), file=out)
345        # these entries provide for reverse DIRDEPS lookup
346        for f in self.obj_deps:
347            print('DEPDIRS_%s += %s' % (f, self.reldir), file=out)
348
349    def seenit(self, dir):
350        """rememer that we have seen dir."""
351        self.seen[dir] = 1
352
353    def add(self, list, data, clue=''):
354        """add data to list if it isn't already there."""
355        if data not in list:
356            list.append(data)
357            if self.debug:
358                print("%s: %sAdd: %s" % (self.name, clue, data), file=self.debug_out)
359
360    def find_top(self, path, list):
361        """the logical tree may be split across multiple trees"""
362        for top in list:
363            if path.startswith(top):
364                if self.debug > 2:
365                    print("found in", top, file=self.debug_out)
366                return top
367        return None
368
369    def find_obj(self, objroot, dir, path, input):
370        """return path within objroot, taking care of .dirdep files"""
371        ddep = None
372        for ddepf in [path + '.dirdep', dir + '/.dirdep']:
373            if not ddep and os.path.exists(ddepf):
374                ddep = open(ddepf, 'r').readline().strip('# \n')
375                if self.debug > 1:
376                    print("found %s: %s\n" % (ddepf, ddep), file=self.debug_out)
377                for e in self.exts:
378                    if ddep.endswith(e):
379                        ddep = ddep[0:-len(e)]
380                        break
381
382        if not ddep:
383            # no .dirdeps, so remember that we've seen the raw input
384            self.seenit(input)
385            self.seenit(dir)
386            if self.machine == 'none':
387                if dir.startswith(objroot):
388                    return dir.replace(objroot,'')
389                return None
390            m = self.dirdep_re.match(dir.replace(objroot,''))
391            if m:
392                ddep = m.group(2)
393                dmachine = m.group(1)
394                if dmachine != self.machine:
395                    if not (self.machine == 'host' and
396                            dmachine == self.host_target):
397                        if self.debug > 2:
398                            print("adding .%s to %s" % (dmachine, ddep), file=self.debug_out)
399                        ddep += '.' + dmachine
400
401        return ddep
402
403    def try_parse(self, name=None, file=None):
404        """give file and line number causing exception"""
405        try:
406            self.parse(name, file)
407        except:
408            # give a useful clue
409            print('{}:{}: '.format(self.name, self.line), end=' ', file=sys.stderr)
410            raise
411
412    def parse(self, name=None, file=None):
413        """A meta file looks like:
414
415        # Meta data file "path"
416        CMD "command-line"
417        CWD "cwd"
418        TARGET "target"
419        -- command output --
420        -- filemon acquired metadata --
421        # buildmon version 3
422        V 3
423        C "pid" "cwd"
424        E "pid" "path"
425        F "pid" "child"
426        R "pid" "path"
427        W "pid" "path"
428        X "pid" "status"
429        D "pid" "path"
430        L "pid" "src" "target"
431        M "pid" "old" "new"
432        S "pid" "path"
433        # Bye bye
434
435        We go to some effort to avoid processing a dependency more than once.
436        Of the above record types only C,E,F,L,R,V and W are of interest.
437        """
438
439        version = 0                     # unknown
440        if name:
441            self.name = name;
442        if file:
443            f = file
444            cwd = self.last_dir = self.cwd
445        else:
446            f = open(self.name, 'r')
447        skip = True
448        pid_cwd = {}
449        pid_last_dir = {}
450        last_pid = 0
451        eof_token = False
452
453        self.line = 0
454        if self.curdir:
455            self.seenit(self.curdir)    # we ignore this
456
457        interesting = '#CEFLRVX'
458        for line in f:
459            self.line += 1
460            # ignore anything we don't care about
461            if not line[0] in interesting:
462                continue
463            if self.debug > 2:
464                print("input:", line, end=' ', file=self.debug_out)
465            w = line.split()
466
467            if skip:
468                if w[0] == 'V':
469                    skip = False
470                    version = int(w[1])
471                    """
472                    if version < 4:
473                        # we cannot ignore 'W' records
474                        # as they may be 'rw'
475                        interesting += 'W'
476                    """
477                elif w[0] == 'CWD':
478                    self.cwd = cwd = self.last_dir = w[1]
479                    self.seenit(cwd)    # ignore this
480                    if self.debug:
481                        print("%s: CWD=%s" % (self.name, cwd), file=self.debug_out)
482                continue
483
484            if w[0] == '#':
485                # check the file has not been truncated
486                if line.find('Bye') > 0:
487                    eof_token = True
488                continue
489
490            pid = int(w[1])
491            if pid != last_pid:
492                if last_pid:
493                    pid_last_dir[last_pid] = self.last_dir
494                cwd = pid_cwd.get(pid, self.cwd)
495                self.last_dir = pid_last_dir.get(pid, self.cwd)
496                last_pid = pid
497
498            # process operations
499            if w[0] == 'F':
500                npid = int(w[2])
501                pid_cwd[npid] = cwd
502                pid_last_dir[npid] = cwd
503                last_pid = npid
504                continue
505            elif w[0] == 'C':
506                cwd = abspath(w[2], cwd, None, self.debug, self.debug_out)
507                if not cwd:
508                    cwd = w[2]
509                    if self.debug > 1:
510                        print("missing cwd=", cwd, file=self.debug_out)
511                if cwd.endswith('/.'):
512                    cwd = cwd[0:-2]
513                self.last_dir = pid_last_dir[pid] = cwd
514                pid_cwd[pid] = cwd
515                if self.debug > 1:
516                    print("cwd=", cwd, file=self.debug_out)
517                continue
518
519            if w[0] == 'X':
520                try:
521                    del self.pids[pid]
522                except KeyError:
523                    pass
524                continue
525
526            if w[2] in self.seen:
527                if self.debug > 2:
528                    print("seen:", w[2], file=self.debug_out)
529                continue
530            # file operations
531            if w[0] in 'ML':
532                # these are special, tread src as read and
533                # target as write
534                self.parse_path(w[2].strip("'"), cwd, 'R', w)
535                self.parse_path(w[3].strip("'"), cwd, 'W', w)
536                continue
537            elif w[0] in 'ERWS':
538                path = w[2]
539                if w[0] == 'E':
540                    self.pids[pid] = path
541                elif path == '.':
542                    continue
543                self.parse_path(path, cwd, w[0], w)
544
545        if version == 0:
546            raise AssertionError('missing filemon data')
547        if not eof_token:
548            raise AssertionError('truncated filemon data')
549
550        setid_pids = []
551        # self.pids should be empty!
552        for pid,path in self.pids.items():
553            try:
554                # no guarantee that path is still valid
555                if os.stat(path).st_mode & (stat.S_ISUID|stat.S_ISGID):
556                    # we do not expect anything after Exec
557                    setid_pids.append(pid)
558                    continue
559            except:
560                # we do not care why the above fails,
561                # we do not want to miss the ERROR below.
562                pass
563            print("ERROR: missing eXit for {} pid {}".format(path, pid))
564        for pid in setid_pids:
565            del self.pids[pid]
566        assert(len(self.pids) == 0)
567        if not file:
568            f.close()
569
570    def is_src(self, base, dir, rdir):
571        """is base in srctop"""
572        for dir in [dir,rdir]:
573            if not dir:
574                continue
575            path = '/'.join([dir,base])
576            srctop = self.find_top(path, self.srctops)
577            if srctop:
578                if self.dpdeps:
579                    self.add(self.file_deps, path.replace(srctop,''), 'file')
580                self.add(self.src_deps, dir.replace(srctop,''), 'src')
581                self.seenit(dir)
582                return True
583        return False
584
585    def parse_path(self, path, cwd, op=None, w=[]):
586        """look at a path for the op specified"""
587
588        if not op:
589            op = w[0]
590
591        # we are never interested in .dirdep files as dependencies
592        if path.endswith('.dirdep'):
593            return
594        for p in self.excludes:
595            if p and path.startswith(p):
596                if self.debug > 2:
597                    print("exclude:", p, path, file=self.debug_out)
598                return
599        # we don't want to resolve the last component if it is
600        # a symlink
601        path = resolve(path, cwd, self.last_dir, self.debug, self.debug_out)
602        if not path:
603            return
604        dir,base = os.path.split(path)
605        if dir in self.seen:
606            if self.debug > 2:
607                print("seen:", dir, file=self.debug_out)
608            return
609        # we can have a path in an objdir which is a link
610        # to the src dir, we may need to add dependencies for each
611        rdir = dir
612        dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out)
613        if dir:
614            rdir = os.path.realpath(dir)
615        else:
616            dir = rdir
617        if rdir == dir:
618            rdir = None
619        # now put path back together
620        path = '/'.join([dir,base])
621        if self.debug > 1:
622            print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out)
623        if op in 'RWS':
624            if path in [self.last_dir, cwd, self.cwd, self.curdir]:
625                if self.debug > 1:
626                    print("skipping:", path, file=self.debug_out)
627                return
628            if os.path.isdir(path):
629                if op in 'RW':
630                    self.last_dir = path;
631                if self.debug > 1:
632                    print("ldir=", self.last_dir, file=self.debug_out)
633                return
634
635        if op in 'ER':
636            # finally, we get down to it
637            if dir == self.cwd or dir == self.curdir:
638                return
639            if self.is_src(base, dir, rdir):
640                self.seenit(w[2])
641                if not rdir:
642                    return
643
644            objroot = None
645            for dir in [dir,rdir]:
646                if not dir:
647                    continue
648                objroot = self.find_top(dir, self.objroots)
649                if objroot:
650                    break
651            if objroot:
652                ddep = self.find_obj(objroot, dir, path, w[2])
653                if ddep:
654                    self.add(self.obj_deps, ddep, 'obj')
655                    if self.dpdeps and objroot.endswith('/stage/'):
656                        sp = '/'.join(path.replace(objroot,'').split('/')[1:])
657                        self.add(self.file_deps, sp, 'file')
658            else:
659                # don't waste time looking again
660                self.seenit(w[2])
661                self.seenit(dir)
662
663
664def main(argv, klass=MetaFile, xopts='', xoptf=None):
665    """Simple driver for class MetaFile.
666
667    Usage:
668        script [options] [key=value ...] "meta" ...
669
670    Options and key=value pairs contribute to the
671    dictionary passed to MetaFile.
672
673    -S "SRCTOP"
674                add "SRCTOP" to the "SRCTOPS" list.
675
676    -C "CURDIR"
677
678    -O "OBJROOT"
679                add "OBJROOT" to the "OBJROOTS" list.
680
681    -m "MACHINE"
682
683    -a "MACHINE_ARCH"
684
685    -H "HOST_TARGET"
686
687    -D "DPDEPS"
688
689    -d  bumps debug level
690
691    """
692    import getopt
693
694    # import Psyco if we can
695    # it can speed things up quite a bit
696    have_psyco = 0
697    try:
698        import psyco
699        psyco.full()
700        have_psyco = 1
701    except:
702        pass
703
704    conf = {
705        'SRCTOPS': [],
706        'OBJROOTS': [],
707        'EXCLUDES': [],
708        }
709
710    try:
711        machine = os.environ['MACHINE']
712        if machine:
713            conf['MACHINE'] = machine
714        machine_arch = os.environ['MACHINE_ARCH']
715        if machine_arch:
716            conf['MACHINE_ARCH'] = machine_arch
717        srctop = os.environ['SB_SRC']
718        if srctop:
719            conf['SRCTOPS'].append(srctop)
720        objroot = os.environ['SB_OBJROOT']
721        if objroot:
722            conf['OBJROOTS'].append(objroot)
723    except:
724        pass
725
726    debug = 0
727    output = True
728
729    opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
730    for o, a in opts:
731        if o == '-a':
732            conf['MACHINE_ARCH'] = a
733        elif o == '-d':
734            debug += 1
735        elif o == '-q':
736            output = False
737        elif o == '-H':
738            conf['HOST_TARGET'] = a
739        elif o == '-S':
740            if a not in conf['SRCTOPS']:
741                conf['SRCTOPS'].append(a)
742        elif o == '-C':
743            conf['CURDIR'] = a
744        elif o == '-O':
745            if a not in conf['OBJROOTS']:
746                conf['OBJROOTS'].append(a)
747        elif o == '-R':
748            conf['RELDIR'] = a
749        elif o == '-D':
750            conf['DPDEPS'] = a
751        elif o == '-m':
752            conf['MACHINE'] = a
753        elif o == '-T':
754            conf['TARGET_SPEC'] = a
755        elif o == '-X':
756            if a not in conf['EXCLUDES']:
757                conf['EXCLUDES'].append(a)
758        elif xoptf:
759            xoptf(o, a, conf)
760
761    conf['debug'] = debug
762
763    # get any var=val assignments
764    eaten = []
765    for a in args:
766        if a.find('=') > 0:
767            k,v = a.split('=')
768            if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']:
769                if k == 'SRCTOP':
770                    k = 'SRCTOPS'
771                elif k == 'OBJROOT':
772                    k = 'OBJROOTS'
773                if v not in conf[k]:
774                    conf[k].append(v)
775            else:
776                conf[k] = v
777            eaten.append(a)
778            continue
779        break
780
781    for a in eaten:
782        args.remove(a)
783
784    debug_out = conf.get('debug_out', sys.stderr)
785
786    if debug:
787        print("config:", file=debug_out)
788        print("psyco=", have_psyco, file=debug_out)
789        for k,v in list(conf.items()):
790            print("%s=%s" % (k,v), file=debug_out)
791
792    m = None
793    for a in args:
794        if a.endswith('.meta'):
795            if not os.path.exists(a):
796                continue
797            m = klass(a, conf)
798        elif a.startswith('@'):
799            # there can actually multiple files per line
800            for line in open(a[1:]):
801                for f in line.strip().split():
802                    if not os.path.exists(f):
803                        continue
804                    m = klass(f, conf)
805
806    if output and m:
807        print(m.dirdeps())
808
809        print(m.src_dirdeps('\nsrc:'))
810
811        dpdeps = conf.get('DPDEPS')
812        if dpdeps:
813            m.file_depends(open(dpdeps, 'w'))
814
815    return m
816
817if __name__ == '__main__':
818    try:
819        main(sys.argv)
820    except:
821        # yes, this goes to stdout
822        print("ERROR: ", sys.exc_info()[1])
823        raise
824
825