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