xref: /freebsd/contrib/bmake/mk/meta2deps.py (revision b79bd43f9ab3bf0dfd4744dee406782f0329134a)
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.51 2025/05/16 20:03:43 sjg Exp $
43
44	Copyright (c) 2011-2025, 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[3].strip("'"), cwd, 'W', w)
547                self.parse_path(w[2].strip("'"), cwd, 'R', 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        npath = resolve(path, cwd, self.last_dir, self.debug, self.debug_out)
615        if not npath:
616            if len(w) > 3 and w[0] in 'ML' and op == 'R' and path.startswith('../'):
617                # we already resolved the target of the M/L
618                # so it makes sense to try and resolve relative to that dir.
619                if os.path.isdir(self.last_path):
620                    dir = self.last_path
621                else:
622                    dir,junk = os.path.split(self.last_path)
623                npath = resolve(path, cwd, dir, self.debug, self.debug_out)
624            if not npath:
625                return
626        path = npath
627        dir,base = os.path.split(path)
628        if dir in self.seen:
629            if self.debug > 2:
630                print("seen:", dir, file=self.debug_out)
631            return
632        # we can have a path in an objdir which is a link
633        # to the src dir, we may need to add dependencies for each
634        rdir = dir
635        dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out)
636        if dir:
637            rdir = os.path.realpath(dir)
638        else:
639            dir = rdir
640        if rdir == dir:
641            rdir = None
642        # now put path back together
643        path = '/'.join([dir,base])
644        self.last_path = path
645        if self.debug > 1:
646            print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out)
647        if op in 'RWS':
648            if path in [self.last_dir, cwd, self.cwd, self.curdir]:
649                if self.debug > 1:
650                    print("skipping:", path, file=self.debug_out)
651                return
652            if os.path.isdir(path):
653                if op in 'RW':
654                    self.last_dir = path;
655                if self.debug > 1:
656                    print("ldir=", self.last_dir, file=self.debug_out)
657                return
658
659        if op in 'ER':
660            # finally, we get down to it
661            if dir == self.cwd or dir == self.curdir:
662                return
663            if self.is_src(base, dir, rdir):
664                self.seenit(w[2])
665                if not rdir:
666                    return
667
668            objroot = None
669            for dir in [dir,rdir]:
670                if not dir:
671                    continue
672                objroot = self.find_top(dir, self.objroots)
673                if objroot:
674                    break
675            if objroot:
676                ddep = self.find_obj(objroot, dir, path, w[2])
677                if ddep:
678                    self.add(self.obj_deps, ddep, 'obj')
679                    if self.dpdeps and objroot.endswith('/stage/'):
680                        sp = '/'.join(path.replace(objroot,'').split('/')[1:])
681                        self.add(self.file_deps, sp, 'file')
682            else:
683                # don't waste time looking again
684                self.seenit(w[2])
685                self.seenit(dir)
686
687
688def main(argv, klass=MetaFile, xopts='', xoptf=None):
689    """Simple driver for class MetaFile.
690
691    Usage:
692        script [options] [key=value ...] "meta" ...
693
694    Options and key=value pairs contribute to the
695    dictionary passed to MetaFile.
696
697    -S "SRCTOP"
698                add "SRCTOP" to the "SRCTOPS" list.
699
700    -C "CURDIR"
701
702    -O "OBJROOT"
703                add "OBJROOT" to the "OBJROOTS" list.
704
705    -m "MACHINE"
706
707    -a "MACHINE_ARCH"
708
709    -H "HOST_TARGET"
710
711    -D "DPDEPS"
712
713    -d  bumps debug level
714
715    """
716    import getopt
717
718    # import Psyco if we can
719    # it can speed things up quite a bit
720    have_psyco = 0
721    try:
722        import psyco
723        psyco.full()
724        have_psyco = 1
725    except:
726        pass
727
728    conf = {
729        'SRCTOPS': [],
730        'OBJROOTS': [],
731        'EXCLUDES': [],
732        }
733
734    conf['SB'] = os.getenv('SB', '')
735
736    try:
737        machine = os.environ['MACHINE']
738        if machine:
739            conf['MACHINE'] = machine
740        machine_arch = os.environ['MACHINE_ARCH']
741        if machine_arch:
742            conf['MACHINE_ARCH'] = machine_arch
743        srctop = os.environ['SB_SRC']
744        if srctop:
745            conf['SRCTOPS'].append(srctop)
746        objroot = os.environ['SB_OBJROOT']
747        if objroot:
748            conf['OBJROOTS'].append(objroot)
749    except:
750        pass
751
752    debug = 0
753    output = True
754
755    opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
756    for o, a in opts:
757        if o == '-a':
758            conf['MACHINE_ARCH'] = a
759        elif o == '-d':
760            debug += 1
761        elif o == '-q':
762            output = False
763        elif o == '-H':
764            conf['HOST_TARGET'] = a
765        elif o == '-S':
766            if a not in conf['SRCTOPS']:
767                conf['SRCTOPS'].append(a)
768        elif o == '-C':
769            conf['CURDIR'] = a
770        elif o == '-O':
771            if a not in conf['OBJROOTS']:
772                conf['OBJROOTS'].append(a)
773        elif o == '-R':
774            conf['RELDIR'] = a
775        elif o == '-D':
776            conf['DPDEPS'] = a
777        elif o == '-m':
778            conf['MACHINE'] = a
779        elif o == '-T':
780            conf['TARGET_SPEC'] = a
781        elif o == '-X':
782            if a not in conf['EXCLUDES']:
783                conf['EXCLUDES'].append(a)
784        elif xoptf:
785            xoptf(o, a, conf)
786
787    conf['debug'] = debug
788
789    # get any var=val assignments
790    eaten = []
791    for a in args:
792        if a.find('=') > 0:
793            k,v = a.split('=')
794            if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']:
795                if k == 'SRCTOP':
796                    k = 'SRCTOPS'
797                elif k == 'OBJROOT':
798                    k = 'OBJROOTS'
799                if v not in conf[k]:
800                    conf[k].append(v)
801            else:
802                conf[k] = v
803            eaten.append(a)
804            continue
805        break
806
807    for a in eaten:
808        args.remove(a)
809
810    debug_out = conf.get('debug_out', sys.stderr)
811
812    if debug:
813        print("config:", file=debug_out)
814        print("psyco=", have_psyco, file=debug_out)
815        for k,v in list(conf.items()):
816            print("%s=%s" % (k,v), file=debug_out)
817
818    m = None
819    for a in args:
820        if a.endswith('.meta'):
821            if not os.path.exists(a):
822                continue
823            m = klass(a, conf)
824        elif a.startswith('@'):
825            # there can actually multiple files per line
826            for line in open(a[1:]):
827                for f in line.strip().split():
828                    if not os.path.exists(f):
829                        continue
830                    m = klass(f, conf)
831
832    if output and m:
833        print(m.dirdeps())
834
835        print(m.src_dirdeps('\nsrc:'))
836
837        dpdeps = conf.get('DPDEPS')
838        if dpdeps:
839            m.file_depends(open(dpdeps, 'w'))
840
841    return m
842
843if __name__ == '__main__':
844    try:
845        main(sys.argv)
846    except:
847        # yes, this goes to stdout
848        print("ERROR: ", sys.exc_info()[1])
849        raise
850
851