xref: /freebsd/contrib/bmake/mk/meta2deps.py (revision 5956d97f4b3204318ceb6aa9c77bd0bc6ea87a41)
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.44 2022/01/29 02:42:01 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
452        self.line = 0
453        if self.curdir:
454            self.seenit(self.curdir)    # we ignore this
455
456        interesting = 'CEFLRVX'
457        for line in f:
458            self.line += 1
459            # ignore anything we don't care about
460            if not line[0] in interesting:
461                continue
462            if self.debug > 2:
463                print("input:", line, end=' ', file=self.debug_out)
464            w = line.split()
465
466            if skip:
467                if w[0] == 'V':
468                    skip = False
469                    version = int(w[1])
470                    """
471                    if version < 4:
472                        # we cannot ignore 'W' records
473                        # as they may be 'rw'
474                        interesting += 'W'
475                    """
476                elif w[0] == 'CWD':
477                    self.cwd = cwd = self.last_dir = w[1]
478                    self.seenit(cwd)    # ignore this
479                    if self.debug:
480                        print("%s: CWD=%s" % (self.name, cwd), file=self.debug_out)
481                continue
482
483            pid = int(w[1])
484            if pid != last_pid:
485                if last_pid:
486                    pid_last_dir[last_pid] = self.last_dir
487                cwd = pid_cwd.get(pid, self.cwd)
488                self.last_dir = pid_last_dir.get(pid, self.cwd)
489                last_pid = pid
490
491            # process operations
492            if w[0] == 'F':
493                npid = int(w[2])
494                pid_cwd[npid] = cwd
495                pid_last_dir[npid] = cwd
496                last_pid = npid
497                continue
498            elif w[0] == 'C':
499                cwd = abspath(w[2], cwd, None, self.debug, self.debug_out)
500                if not cwd:
501                    cwd = w[2]
502                    if self.debug > 1:
503                        print("missing cwd=", cwd, file=self.debug_out)
504                if cwd.endswith('/.'):
505                    cwd = cwd[0:-2]
506                self.last_dir = pid_last_dir[pid] = cwd
507                pid_cwd[pid] = cwd
508                if self.debug > 1:
509                    print("cwd=", cwd, file=self.debug_out)
510                continue
511
512            if w[0] == 'X':
513                try:
514                    del self.pids[pid]
515                except KeyError:
516                    pass
517                continue
518
519            if w[2] in self.seen:
520                if self.debug > 2:
521                    print("seen:", w[2], file=self.debug_out)
522                continue
523            # file operations
524            if w[0] in 'ML':
525                # these are special, tread src as read and
526                # target as write
527                self.parse_path(w[2].strip("'"), cwd, 'R', w)
528                self.parse_path(w[3].strip("'"), cwd, 'W', w)
529                continue
530            elif w[0] in 'ERWS':
531                path = w[2]
532                if w[0] == 'E':
533                    self.pids[pid] = path
534                elif path == '.':
535                    continue
536                self.parse_path(path, cwd, w[0], w)
537
538        assert(version > 0)
539        setid_pids = []
540        # self.pids should be empty!
541        for pid,path in self.pids.items():
542            try:
543                # no guarantee that path is still valid
544                if os.stat(path).st_mode & (stat.S_ISUID|stat.S_ISGID):
545                    # we do not expect anything after Exec
546                    setid_pids.append(pid)
547                    continue
548            except:
549                # we do not care why the above fails,
550                # we do not want to miss the ERROR below.
551                pass
552            print("ERROR: missing eXit for {} pid {}".format(path, pid))
553        for pid in setid_pids:
554            del self.pids[pid]
555        assert(len(self.pids) == 0)
556        if not file:
557            f.close()
558
559    def is_src(self, base, dir, rdir):
560        """is base in srctop"""
561        for dir in [dir,rdir]:
562            if not dir:
563                continue
564            path = '/'.join([dir,base])
565            srctop = self.find_top(path, self.srctops)
566            if srctop:
567                if self.dpdeps:
568                    self.add(self.file_deps, path.replace(srctop,''), 'file')
569                self.add(self.src_deps, dir.replace(srctop,''), 'src')
570                self.seenit(dir)
571                return True
572        return False
573
574    def parse_path(self, path, cwd, op=None, w=[]):
575        """look at a path for the op specified"""
576
577        if not op:
578            op = w[0]
579
580        # we are never interested in .dirdep files as dependencies
581        if path.endswith('.dirdep'):
582            return
583        for p in self.excludes:
584            if p and path.startswith(p):
585                if self.debug > 2:
586                    print("exclude:", p, path, file=self.debug_out)
587                return
588        # we don't want to resolve the last component if it is
589        # a symlink
590        path = resolve(path, cwd, self.last_dir, self.debug, self.debug_out)
591        if not path:
592            return
593        dir,base = os.path.split(path)
594        if dir in self.seen:
595            if self.debug > 2:
596                print("seen:", dir, file=self.debug_out)
597            return
598        # we can have a path in an objdir which is a link
599        # to the src dir, we may need to add dependencies for each
600        rdir = dir
601        dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out)
602        if dir:
603            rdir = os.path.realpath(dir)
604        else:
605            dir = rdir
606        if rdir == dir:
607            rdir = None
608        # now put path back together
609        path = '/'.join([dir,base])
610        if self.debug > 1:
611            print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out)
612        if op in 'RWS':
613            if path in [self.last_dir, cwd, self.cwd, self.curdir]:
614                if self.debug > 1:
615                    print("skipping:", path, file=self.debug_out)
616                return
617            if os.path.isdir(path):
618                if op in 'RW':
619                    self.last_dir = path;
620                if self.debug > 1:
621                    print("ldir=", self.last_dir, file=self.debug_out)
622                return
623
624        if op in 'ER':
625            # finally, we get down to it
626            if dir == self.cwd or dir == self.curdir:
627                return
628            if self.is_src(base, dir, rdir):
629                self.seenit(w[2])
630                if not rdir:
631                    return
632
633            objroot = None
634            for dir in [dir,rdir]:
635                if not dir:
636                    continue
637                objroot = self.find_top(dir, self.objroots)
638                if objroot:
639                    break
640            if objroot:
641                ddep = self.find_obj(objroot, dir, path, w[2])
642                if ddep:
643                    self.add(self.obj_deps, ddep, 'obj')
644                    if self.dpdeps and objroot.endswith('/stage/'):
645                        sp = '/'.join(path.replace(objroot,'').split('/')[1:])
646                        self.add(self.file_deps, sp, 'file')
647            else:
648                # don't waste time looking again
649                self.seenit(w[2])
650                self.seenit(dir)
651
652
653def main(argv, klass=MetaFile, xopts='', xoptf=None):
654    """Simple driver for class MetaFile.
655
656    Usage:
657        script [options] [key=value ...] "meta" ...
658
659    Options and key=value pairs contribute to the
660    dictionary passed to MetaFile.
661
662    -S "SRCTOP"
663                add "SRCTOP" to the "SRCTOPS" list.
664
665    -C "CURDIR"
666
667    -O "OBJROOT"
668                add "OBJROOT" to the "OBJROOTS" list.
669
670    -m "MACHINE"
671
672    -a "MACHINE_ARCH"
673
674    -H "HOST_TARGET"
675
676    -D "DPDEPS"
677
678    -d  bumps debug level
679
680    """
681    import getopt
682
683    # import Psyco if we can
684    # it can speed things up quite a bit
685    have_psyco = 0
686    try:
687        import psyco
688        psyco.full()
689        have_psyco = 1
690    except:
691        pass
692
693    conf = {
694        'SRCTOPS': [],
695        'OBJROOTS': [],
696        'EXCLUDES': [],
697        }
698
699    try:
700        machine = os.environ['MACHINE']
701        if machine:
702            conf['MACHINE'] = machine
703        machine_arch = os.environ['MACHINE_ARCH']
704        if machine_arch:
705            conf['MACHINE_ARCH'] = machine_arch
706        srctop = os.environ['SB_SRC']
707        if srctop:
708            conf['SRCTOPS'].append(srctop)
709        objroot = os.environ['SB_OBJROOT']
710        if objroot:
711            conf['OBJROOTS'].append(objroot)
712    except:
713        pass
714
715    debug = 0
716    output = True
717
718    opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
719    for o, a in opts:
720        if o == '-a':
721            conf['MACHINE_ARCH'] = a
722        elif o == '-d':
723            debug += 1
724        elif o == '-q':
725            output = False
726        elif o == '-H':
727            conf['HOST_TARGET'] = a
728        elif o == '-S':
729            if a not in conf['SRCTOPS']:
730                conf['SRCTOPS'].append(a)
731        elif o == '-C':
732            conf['CURDIR'] = a
733        elif o == '-O':
734            if a not in conf['OBJROOTS']:
735                conf['OBJROOTS'].append(a)
736        elif o == '-R':
737            conf['RELDIR'] = a
738        elif o == '-D':
739            conf['DPDEPS'] = a
740        elif o == '-m':
741            conf['MACHINE'] = a
742        elif o == '-T':
743            conf['TARGET_SPEC'] = a
744        elif o == '-X':
745            if a not in conf['EXCLUDES']:
746                conf['EXCLUDES'].append(a)
747        elif xoptf:
748            xoptf(o, a, conf)
749
750    conf['debug'] = debug
751
752    # get any var=val assignments
753    eaten = []
754    for a in args:
755        if a.find('=') > 0:
756            k,v = a.split('=')
757            if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']:
758                if k == 'SRCTOP':
759                    k = 'SRCTOPS'
760                elif k == 'OBJROOT':
761                    k = 'OBJROOTS'
762                if v not in conf[k]:
763                    conf[k].append(v)
764            else:
765                conf[k] = v
766            eaten.append(a)
767            continue
768        break
769
770    for a in eaten:
771        args.remove(a)
772
773    debug_out = conf.get('debug_out', sys.stderr)
774
775    if debug:
776        print("config:", file=debug_out)
777        print("psyco=", have_psyco, file=debug_out)
778        for k,v in list(conf.items()):
779            print("%s=%s" % (k,v), file=debug_out)
780
781    m = None
782    for a in args:
783        if a.endswith('.meta'):
784            if not os.path.exists(a):
785                continue
786            m = klass(a, conf)
787        elif a.startswith('@'):
788            # there can actually multiple files per line
789            for line in open(a[1:]):
790                for f in line.strip().split():
791                    if not os.path.exists(f):
792                        continue
793                    m = klass(f, conf)
794
795    if output and m:
796        print(m.dirdeps())
797
798        print(m.src_dirdeps('\nsrc:'))
799
800        dpdeps = conf.get('DPDEPS')
801        if dpdeps:
802            m.file_depends(open(dpdeps, 'w'))
803
804    return m
805
806if __name__ == '__main__':
807    try:
808        main(sys.argv)
809    except:
810        # yes, this goes to stdout
811        print("ERROR: ", sys.exc_info()[1])
812        raise
813
814