xref: /freebsd/share/mk/meta2deps.py (revision c8245ceb47565e864a350e6aa33ba9a6461ebe02)
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.50 2024/09/27 00:08:36 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            self.sb = conf.get('SB', '')
298            # we want the longest match
299            self.srctops.sort(reverse=True)
300            self.objroots.sort(reverse=True)
301
302            self.excludes = conf.get('EXCLUDES', [])
303
304            if self.debug:
305                print("host_target=", self.host_target, file=self.debug_out)
306                print("srctops=", self.srctops, file=self.debug_out)
307                print("objroots=", self.objroots, file=self.debug_out)
308                print("excludes=", self.excludes, file=self.debug_out)
309                print("ext_list=", self.exts, file=self.debug_out)
310
311            self.dirdep_re = re.compile(r'([^/]+)/(.+)')
312
313        if self.dpdeps and not self.reldir:
314            if self.debug:
315                print("need reldir:", end=' ', file=self.debug_out)
316            if self.curdir:
317                srctop = self.find_top(self.curdir, self.srctops)
318                if srctop:
319                    self.reldir = self.curdir.replace(srctop,'')
320                    if self.debug:
321                        print(self.reldir, file=self.debug_out)
322            if not self.reldir:
323                self.dpdeps = None      # we cannot do it?
324
325        self.cwd = os.getcwd()          # make sure this is initialized
326        self.last_dir = self.cwd
327
328        if name:
329            self.try_parse()
330
331    def reset(self):
332        """reset state if we are being passed meta files from multiple directories."""
333        self.seen = {}
334        self.obj_deps = []
335        self.src_deps = []
336        self.file_deps = []
337
338    def dirdeps(self, sep='\n'):
339        """return DIRDEPS"""
340        return sep.strip() + sep.join(self.obj_deps)
341
342    def src_dirdeps(self, sep='\n'):
343        """return SRC_DIRDEPS"""
344        return sep.strip() + sep.join(self.src_deps)
345
346    def file_depends(self, out=None):
347        """Append DPDEPS_${file} += ${RELDIR}
348        for each file we saw, to the output file."""
349        if not self.reldir:
350            return None
351        for f in sort_unique(self.file_deps):
352            print('DPDEPS_%s += %s' % (f, self.reldir), file=out)
353        # these entries provide for reverse DIRDEPS lookup
354        for f in self.obj_deps:
355            print('DEPDIRS_%s += %s' % (f, self.reldir), file=out)
356
357    def seenit(self, dir):
358        """rememer that we have seen dir."""
359        self.seen[dir] = 1
360
361    def add(self, list, data, clue=''):
362        """add data to list if it isn't already there."""
363        if data not in list:
364            list.append(data)
365            if self.debug:
366                print("%s: %sAdd: %s" % (self.name, clue, data), file=self.debug_out)
367
368    def find_top(self, path, list):
369        """the logical tree may be split across multiple trees"""
370        for top in list:
371            if path.startswith(top):
372                if self.debug > 2:
373                    print("found in", top, file=self.debug_out)
374                return top
375        return None
376
377    def find_obj(self, objroot, dir, path, input):
378        """return path within objroot, taking care of .dirdep files"""
379        ddep = None
380        for ddepf in [path + '.dirdep', dir + '/.dirdep']:
381            if not ddep and os.path.exists(ddepf):
382                ddep = open(ddepf, 'r').readline().strip('# \n')
383                if self.debug > 1:
384                    print("found %s: %s\n" % (ddepf, ddep), file=self.debug_out)
385                for e in self.exts:
386                    if ddep.endswith(e):
387                        ddep = ddep[0:-len(e)]
388                        break
389
390        if not ddep:
391            # no .dirdeps, so remember that we've seen the raw input
392            self.seenit(input)
393            self.seenit(dir)
394            if self.machine == 'none':
395                if dir.startswith(objroot):
396                    return dir.replace(objroot,'')
397                return None
398            m = self.dirdep_re.match(dir.replace(objroot,''))
399            if m:
400                ddep = m.group(2)
401                dmachine = m.group(1)
402                if dmachine != self.machine:
403                    if not (self.machine == 'host' and
404                            dmachine == self.host_target):
405                        if self.debug > 2:
406                            print("adding .%s to %s" % (dmachine, ddep), file=self.debug_out)
407                        ddep += '.' + dmachine
408
409        return ddep
410
411    def try_parse(self, name=None, file=None):
412        """give file and line number causing exception"""
413        try:
414            self.parse(name, file)
415        except:
416            # give a useful clue
417            print('{}:{}: '.format(self.name, self.line), end=' ', file=sys.stderr)
418            raise
419
420    def parse(self, name=None, file=None):
421        """A meta file looks like:
422
423        # Meta data file "path"
424        CMD "command-line"
425        CWD "cwd"
426        TARGET "target"
427        -- command output --
428        -- filemon acquired metadata --
429        # buildmon version 3
430        V 3
431        C "pid" "cwd"
432        E "pid" "path"
433        F "pid" "child"
434        R "pid" "path"
435        W "pid" "path"
436        X "pid" "status"
437        D "pid" "path"
438        L "pid" "src" "target"
439        M "pid" "old" "new"
440        S "pid" "path"
441        # Bye bye
442
443        We go to some effort to avoid processing a dependency more than once.
444        Of the above record types only C,E,F,L,R,V and W are of interest.
445        """
446
447        version = 0                     # unknown
448        if name:
449            self.name = name;
450        if file:
451            f = file
452            cwd = self.last_dir = self.cwd
453        else:
454            f = open(self.name, 'r')
455        skip = True
456        pid_cwd = {}
457        pid_last_dir = {}
458        last_pid = 0
459        eof_token = False
460
461        self.line = 0
462        if self.curdir:
463            self.seenit(self.curdir)    # we ignore this
464
465        if self.sb and self.name.startswith(self.sb):
466            error_name = self.name.replace(self.sb+'/','')
467        else:
468            error_name = self.name
469        interesting = '#CEFLRVX'
470        for line in f:
471            self.line += 1
472            # ignore anything we don't care about
473            if not line[0] in interesting:
474                continue
475            if self.debug > 2:
476                print("input:", line, end=' ', file=self.debug_out)
477            w = line.split()
478
479            if skip:
480                if w[0] == 'V':
481                    skip = False
482                    version = int(w[1])
483                    """
484                    if version < 4:
485                        # we cannot ignore 'W' records
486                        # as they may be 'rw'
487                        interesting += 'W'
488                    """
489                elif w[0] == 'CWD':
490                    self.cwd = cwd = self.last_dir = w[1]
491                    self.seenit(cwd)    # ignore this
492                    if self.debug:
493                        print("%s: CWD=%s" % (self.name, cwd), file=self.debug_out)
494                continue
495
496            if w[0] == '#':
497                # check the file has not been truncated
498                if line.find('Bye') > 0:
499                    eof_token = True
500                continue
501
502            pid = int(w[1])
503            if pid != last_pid:
504                if last_pid:
505                    pid_last_dir[last_pid] = self.last_dir
506                cwd = pid_cwd.get(pid, self.cwd)
507                self.last_dir = pid_last_dir.get(pid, self.cwd)
508                last_pid = pid
509
510            # process operations
511            if w[0] == 'F':
512                npid = int(w[2])
513                pid_cwd[npid] = cwd
514                pid_last_dir[npid] = cwd
515                last_pid = npid
516                continue
517            elif w[0] == 'C':
518                cwd = abspath(w[2], cwd, None, self.debug, self.debug_out)
519                if not cwd:
520                    cwd = w[2]
521                    if self.debug > 1:
522                        print("missing cwd=", cwd, file=self.debug_out)
523                if cwd.endswith('/.'):
524                    cwd = cwd[0:-2]
525                self.last_dir = pid_last_dir[pid] = cwd
526                pid_cwd[pid] = cwd
527                if self.debug > 1:
528                    print("cwd=", cwd, file=self.debug_out)
529                continue
530
531            if w[0] == 'X':
532                try:
533                    del self.pids[pid]
534                except KeyError:
535                    pass
536                continue
537
538            if w[2] in self.seen:
539                if self.debug > 2:
540                    print("seen:", w[2], file=self.debug_out)
541                continue
542            # file operations
543            if w[0] in 'ML':
544                # these are special, tread src as read and
545                # target as write
546                self.parse_path(w[2].strip("'"), cwd, 'R', w)
547                self.parse_path(w[3].strip("'"), cwd, 'W', w)
548                continue
549            elif w[0] in 'ERWS':
550                path = w[2]
551                if w[0] == 'E':
552                    self.pids[pid] = path
553                elif path == '.':
554                    continue
555                self.parse_path(path, cwd, w[0], w)
556
557        if version == 0:
558            raise AssertionError('missing filemon data: {}'.format(error_name))
559        if not eof_token:
560            raise AssertionError('truncated filemon data: {}'.format(error_name))
561
562        setid_pids = []
563        # self.pids should be empty!
564        for pid,path in self.pids.items():
565            try:
566                # no guarantee that path is still valid
567                if os.stat(path).st_mode & (stat.S_ISUID|stat.S_ISGID):
568                    # we do not expect anything after Exec
569                    setid_pids.append(pid)
570                    continue
571            except:
572                # we do not care why the above fails,
573                # we do not want to miss the ERROR below.
574                pass
575            print("ERROR: missing eXit for {} pid {}".format(path, pid))
576        for pid in setid_pids:
577            del self.pids[pid]
578        if len(self.pids) > 0:
579            raise AssertionError('bad filemon data - missing eXits: {}'.format(error_name))
580        if not file:
581            f.close()
582
583    def is_src(self, base, dir, rdir):
584        """is base in srctop"""
585        for dir in [dir,rdir]:
586            if not dir:
587                continue
588            path = '/'.join([dir,base])
589            srctop = self.find_top(path, self.srctops)
590            if srctop:
591                if self.dpdeps:
592                    self.add(self.file_deps, path.replace(srctop,''), 'file')
593                self.add(self.src_deps, dir.replace(srctop,''), 'src')
594                self.seenit(dir)
595                return True
596        return False
597
598    def parse_path(self, path, cwd, op=None, w=[]):
599        """look at a path for the op specified"""
600
601        if not op:
602            op = w[0]
603
604        # we are never interested in .dirdep files as dependencies
605        if path.endswith('.dirdep'):
606            return
607        for p in self.excludes:
608            if p and path.startswith(p):
609                if self.debug > 2:
610                    print("exclude:", p, path, file=self.debug_out)
611                return
612        # we don't want to resolve the last component if it is
613        # a symlink
614        path = resolve(path, cwd, self.last_dir, self.debug, self.debug_out)
615        if not path:
616            return
617        dir,base = os.path.split(path)
618        if dir in self.seen:
619            if self.debug > 2:
620                print("seen:", dir, file=self.debug_out)
621            return
622        # we can have a path in an objdir which is a link
623        # to the src dir, we may need to add dependencies for each
624        rdir = dir
625        dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out)
626        if dir:
627            rdir = os.path.realpath(dir)
628        else:
629            dir = rdir
630        if rdir == dir:
631            rdir = None
632        # now put path back together
633        path = '/'.join([dir,base])
634        if self.debug > 1:
635            print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out)
636        if op in 'RWS':
637            if path in [self.last_dir, cwd, self.cwd, self.curdir]:
638                if self.debug > 1:
639                    print("skipping:", path, file=self.debug_out)
640                return
641            if os.path.isdir(path):
642                if op in 'RW':
643                    self.last_dir = path;
644                if self.debug > 1:
645                    print("ldir=", self.last_dir, file=self.debug_out)
646                return
647
648        if op in 'ER':
649            # finally, we get down to it
650            if dir == self.cwd or dir == self.curdir:
651                return
652            if self.is_src(base, dir, rdir):
653                self.seenit(w[2])
654                if not rdir:
655                    return
656
657            objroot = None
658            for dir in [dir,rdir]:
659                if not dir:
660                    continue
661                objroot = self.find_top(dir, self.objroots)
662                if objroot:
663                    break
664            if objroot:
665                ddep = self.find_obj(objroot, dir, path, w[2])
666                if ddep:
667                    self.add(self.obj_deps, ddep, 'obj')
668                    if self.dpdeps and objroot.endswith('/stage/'):
669                        sp = '/'.join(path.replace(objroot,'').split('/')[1:])
670                        self.add(self.file_deps, sp, 'file')
671            else:
672                # don't waste time looking again
673                self.seenit(w[2])
674                self.seenit(dir)
675
676
677def main(argv, klass=MetaFile, xopts='', xoptf=None):
678    """Simple driver for class MetaFile.
679
680    Usage:
681        script [options] [key=value ...] "meta" ...
682
683    Options and key=value pairs contribute to the
684    dictionary passed to MetaFile.
685
686    -S "SRCTOP"
687                add "SRCTOP" to the "SRCTOPS" list.
688
689    -C "CURDIR"
690
691    -O "OBJROOT"
692                add "OBJROOT" to the "OBJROOTS" list.
693
694    -m "MACHINE"
695
696    -a "MACHINE_ARCH"
697
698    -H "HOST_TARGET"
699
700    -D "DPDEPS"
701
702    -d  bumps debug level
703
704    """
705    import getopt
706
707    # import Psyco if we can
708    # it can speed things up quite a bit
709    have_psyco = 0
710    try:
711        import psyco
712        psyco.full()
713        have_psyco = 1
714    except:
715        pass
716
717    conf = {
718        'SRCTOPS': [],
719        'OBJROOTS': [],
720        'EXCLUDES': [],
721        }
722
723    conf['SB'] = os.getenv('SB', '')
724
725    try:
726        machine = os.environ['MACHINE']
727        if machine:
728            conf['MACHINE'] = machine
729        machine_arch = os.environ['MACHINE_ARCH']
730        if machine_arch:
731            conf['MACHINE_ARCH'] = machine_arch
732        srctop = os.environ['SB_SRC']
733        if srctop:
734            conf['SRCTOPS'].append(srctop)
735        objroot = os.environ['SB_OBJROOT']
736        if objroot:
737            conf['OBJROOTS'].append(objroot)
738    except:
739        pass
740
741    debug = 0
742    output = True
743
744    opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
745    for o, a in opts:
746        if o == '-a':
747            conf['MACHINE_ARCH'] = a
748        elif o == '-d':
749            debug += 1
750        elif o == '-q':
751            output = False
752        elif o == '-H':
753            conf['HOST_TARGET'] = a
754        elif o == '-S':
755            if a not in conf['SRCTOPS']:
756                conf['SRCTOPS'].append(a)
757        elif o == '-C':
758            conf['CURDIR'] = a
759        elif o == '-O':
760            if a not in conf['OBJROOTS']:
761                conf['OBJROOTS'].append(a)
762        elif o == '-R':
763            conf['RELDIR'] = a
764        elif o == '-D':
765            conf['DPDEPS'] = a
766        elif o == '-m':
767            conf['MACHINE'] = a
768        elif o == '-T':
769            conf['TARGET_SPEC'] = a
770        elif o == '-X':
771            if a not in conf['EXCLUDES']:
772                conf['EXCLUDES'].append(a)
773        elif xoptf:
774            xoptf(o, a, conf)
775
776    conf['debug'] = debug
777
778    # get any var=val assignments
779    eaten = []
780    for a in args:
781        if a.find('=') > 0:
782            k,v = a.split('=')
783            if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']:
784                if k == 'SRCTOP':
785                    k = 'SRCTOPS'
786                elif k == 'OBJROOT':
787                    k = 'OBJROOTS'
788                if v not in conf[k]:
789                    conf[k].append(v)
790            else:
791                conf[k] = v
792            eaten.append(a)
793            continue
794        break
795
796    for a in eaten:
797        args.remove(a)
798
799    debug_out = conf.get('debug_out', sys.stderr)
800
801    if debug:
802        print("config:", file=debug_out)
803        print("psyco=", have_psyco, file=debug_out)
804        for k,v in list(conf.items()):
805            print("%s=%s" % (k,v), file=debug_out)
806
807    m = None
808    for a in args:
809        if a.endswith('.meta'):
810            if not os.path.exists(a):
811                continue
812            m = klass(a, conf)
813        elif a.startswith('@'):
814            # there can actually multiple files per line
815            for line in open(a[1:]):
816                for f in line.strip().split():
817                    if not os.path.exists(f):
818                        continue
819                    m = klass(f, conf)
820
821    if output and m:
822        print(m.dirdeps())
823
824        print(m.src_dirdeps('\nsrc:'))
825
826        dpdeps = conf.get('DPDEPS')
827        if dpdeps:
828            m.file_depends(open(dpdeps, 'w'))
829
830    return m
831
832if __name__ == '__main__':
833    try:
834        main(sys.argv)
835    except:
836        # yes, this goes to stdout
837        print("ERROR: ", sys.exc_info()[1])
838        raise
839
840