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