1#!/usr/bin/env python 2 3from __future__ import print_function 4 5""" 6This script parses each "meta" file and extracts the 7information needed to deduce build and src dependencies. 8 9It works much the same as the original shell script, but is 10*much* more efficient. 11 12The parsing work is handled by the class MetaFile. 13We only pay attention to a subset of the information in the 14"meta" files. Specifically: 15 16'CWD' to initialize our notion. 17 18'C' to track chdir(2) on a per process basis 19 20'R' files read are what we really care about. 21 directories read, provide a clue to resolving 22 subsequent relative paths. That is if we cannot find 23 them relative to 'cwd', we check relative to the last 24 dir read. 25 26'W' files opened for write or read-write, 27 for filemon V3 and earlier. 28 29'E' files executed. 30 31'L' files linked 32 33'V' the filemon version, this record is used as a clue 34 that we have reached the interesting bit. 35 36""" 37 38""" 39RCSid: 40 $Id: meta2deps.py,v 1.45 2023/01/18 01:35:24 sjg Exp $ 41 42 Copyright (c) 2011-2020, Simon J. Gerraty 43 Copyright (c) 2011-2017, Juniper Networks, Inc. 44 All rights reserved. 45 46 Redistribution and use in source and binary forms, with or without 47 modification, are permitted provided that the following conditions 48 are met: 49 1. Redistributions of source code must retain the above copyright 50 notice, this list of conditions and the following disclaimer. 51 2. Redistributions in binary form must reproduce the above copyright 52 notice, this list of conditions and the following disclaimer in the 53 documentation and/or other materials provided with the distribution. 54 55 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 56 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 57 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 58 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 59 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 60 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 61 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 62 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 63 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 64 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 65 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 66 67""" 68 69import os 70import re 71import sys 72import stat 73 74def resolve(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr): 75 """ 76 Return an absolute path, resolving via cwd or last_dir if needed. 77 """ 78 if path.endswith('/.'): 79 path = path[0:-2] 80 if len(path) > 0 and path[0] == '/': 81 if os.path.exists(path): 82 return path 83 if debug > 2: 84 print("skipping non-existent:", path, file=debug_out) 85 return None 86 if path == '.': 87 return cwd 88 if path.startswith('./'): 89 return cwd + path[1:] 90 if last_dir == cwd: 91 last_dir = None 92 for d in [last_dir, cwd]: 93 if not d: 94 continue 95 if path == '..': 96 dw = d.split('/') 97 p = '/'.join(dw[:-1]) 98 if not p: 99 p = '/' 100 return p 101 p = '/'.join([d,path]) 102 if debug > 2: 103 print("looking for:", p, end=' ', file=debug_out) 104 if not os.path.exists(p): 105 if debug > 2: 106 print("nope", file=debug_out) 107 p = None 108 continue 109 if debug > 2: 110 print("found:", p, file=debug_out) 111 return p 112 return None 113 114def cleanpath(path): 115 """cleanup path without using realpath(3)""" 116 if path.startswith('/'): 117 r = '/' 118 else: 119 r = '' 120 p = [] 121 w = path.split('/') 122 for d in w: 123 if not d or d == '.': 124 continue 125 if d == '..': 126 try: 127 p.pop() 128 continue 129 except: 130 break 131 p.append(d) 132 133 return r + '/'.join(p) 134 135def abspath(path, cwd, last_dir=None, debug=0, debug_out=sys.stderr): 136 """ 137 Return an absolute path, resolving via cwd or last_dir if needed. 138 this gets called a lot, so we try to avoid calling realpath. 139 """ 140 rpath = resolve(path, cwd, last_dir, debug, debug_out) 141 if rpath: 142 path = rpath 143 elif len(path) > 0 and path[0] == '/': 144 return None 145 if (path.find('/') < 0 or 146 path.find('./') > 0 or 147 path.endswith('/..')): 148 path = cleanpath(path) 149 return path 150 151def sort_unique(list, cmp=None, key=None, reverse=False): 152 if sys.version_info[0] == 2: 153 list.sort(cmp, key, reverse) 154 else: 155 list.sort(reverse=reverse) 156 nl = [] 157 le = None 158 for e in list: 159 if e == le: 160 continue 161 le = e 162 nl.append(e) 163 return nl 164 165def add_trims(x): 166 return ['/' + x + '/', 167 '/' + x, 168 x + '/', 169 x] 170 171def target_spec_exts(target_spec): 172 """return a list of dirdep extensions that could match target_spec""" 173 174 if target_spec.find(',') < 0: 175 return ['.'+target_spec] 176 w = target_spec.split(',') 177 n = len(w) 178 e = [] 179 while n > 0: 180 e.append('.'+','.join(w[0:n])) 181 n -= 1 182 return e 183 184class MetaFile: 185 """class to parse meta files generated by bmake.""" 186 187 conf = None 188 dirdep_re = None 189 host_target = None 190 srctops = [] 191 objroots = [] 192 excludes = [] 193 seen = {} 194 obj_deps = [] 195 src_deps = [] 196 file_deps = [] 197 198 def __init__(self, name, conf={}): 199 """if name is set we will parse it now. 200 conf can have the following keys: 201 202 SRCTOPS list of tops of the src tree(s). 203 204 CURDIR the src directory 'bmake' was run from. 205 206 RELDIR the relative path from SRCTOP to CURDIR 207 208 MACHINE the machine we built for. 209 set to 'none' if we are not cross-building. 210 More specifically if machine cannot be deduced from objdirs. 211 212 TARGET_SPEC 213 Sometimes MACHINE isn't enough. 214 215 HOST_TARGET 216 when we build for the pseudo machine 'host' 217 the object tree uses HOST_TARGET rather than MACHINE. 218 219 OBJROOTS a list of the common prefix for all obj dirs it might 220 end in '/' or '-'. 221 222 DPDEPS names an optional file to which per file dependencies 223 will be appended. 224 For example if 'some/path/foo.h' is read from SRCTOP 225 then 'DPDEPS_some/path/foo.h +=' "RELDIR" is output. 226 This can allow 'bmake' to learn all the dirs within 227 the tree that depend on 'foo.h' 228 229 EXCLUDES 230 A list of paths to ignore. 231 ccache(1) can otherwise be trouble. 232 233 debug desired debug level 234 235 debug_out open file to send debug output to (sys.stderr) 236 237 """ 238 239 self.name = name 240 self.debug = conf.get('debug', 0) 241 self.debug_out = conf.get('debug_out', sys.stderr) 242 243 self.machine = conf.get('MACHINE', '') 244 self.machine_arch = conf.get('MACHINE_ARCH', '') 245 self.target_spec = conf.get('TARGET_SPEC', self.machine) 246 self.exts = target_spec_exts(self.target_spec) 247 self.curdir = conf.get('CURDIR') 248 self.reldir = conf.get('RELDIR') 249 self.dpdeps = conf.get('DPDEPS') 250 self.pids = {} 251 self.line = 0 252 253 if not self.conf: 254 # some of the steps below we want to do only once 255 self.conf = conf 256 self.host_target = conf.get('HOST_TARGET') 257 for srctop in conf.get('SRCTOPS', []): 258 if srctop[-1] != '/': 259 srctop += '/' 260 if not srctop in self.srctops: 261 self.srctops.append(srctop) 262 _srctop = os.path.realpath(srctop) 263 if _srctop[-1] != '/': 264 _srctop += '/' 265 if not _srctop in self.srctops: 266 self.srctops.append(_srctop) 267 268 trim_list = add_trims(self.machine) 269 if self.machine == 'host': 270 trim_list += add_trims(self.host_target) 271 if self.target_spec != self.machine: 272 trim_list += add_trims(self.target_spec) 273 274 for objroot in conf.get('OBJROOTS', []): 275 for e in trim_list: 276 if objroot.endswith(e): 277 # this is not what we want - fix it 278 objroot = objroot[0:-len(e)] 279 280 if objroot[-1] != '/': 281 objroot += '/' 282 if not objroot in self.objroots: 283 self.objroots.append(objroot) 284 _objroot = os.path.realpath(objroot) 285 if objroot[-1] == '/': 286 _objroot += '/' 287 if not _objroot in self.objroots: 288 self.objroots.append(_objroot) 289 290 # we want the longest match 291 self.srctops.sort(reverse=True) 292 self.objroots.sort(reverse=True) 293 294 self.excludes = conf.get('EXCLUDES', []) 295 296 if self.debug: 297 print("host_target=", self.host_target, file=self.debug_out) 298 print("srctops=", self.srctops, file=self.debug_out) 299 print("objroots=", self.objroots, file=self.debug_out) 300 print("excludes=", self.excludes, file=self.debug_out) 301 print("ext_list=", self.exts, file=self.debug_out) 302 303 self.dirdep_re = re.compile(r'([^/]+)/(.+)') 304 305 if self.dpdeps and not self.reldir: 306 if self.debug: 307 print("need reldir:", end=' ', file=self.debug_out) 308 if self.curdir: 309 srctop = self.find_top(self.curdir, self.srctops) 310 if srctop: 311 self.reldir = self.curdir.replace(srctop,'') 312 if self.debug: 313 print(self.reldir, file=self.debug_out) 314 if not self.reldir: 315 self.dpdeps = None # we cannot do it? 316 317 self.cwd = os.getcwd() # make sure this is initialized 318 self.last_dir = self.cwd 319 320 if name: 321 self.try_parse() 322 323 def reset(self): 324 """reset state if we are being passed meta files from multiple directories.""" 325 self.seen = {} 326 self.obj_deps = [] 327 self.src_deps = [] 328 self.file_deps = [] 329 330 def dirdeps(self, sep='\n'): 331 """return DIRDEPS""" 332 return sep.strip() + sep.join(self.obj_deps) 333 334 def src_dirdeps(self, sep='\n'): 335 """return SRC_DIRDEPS""" 336 return sep.strip() + sep.join(self.src_deps) 337 338 def file_depends(self, out=None): 339 """Append DPDEPS_${file} += ${RELDIR} 340 for each file we saw, to the output file.""" 341 if not self.reldir: 342 return None 343 for f in sort_unique(self.file_deps): 344 print('DPDEPS_%s += %s' % (f, self.reldir), file=out) 345 # these entries provide for reverse DIRDEPS lookup 346 for f in self.obj_deps: 347 print('DEPDIRS_%s += %s' % (f, self.reldir), file=out) 348 349 def seenit(self, dir): 350 """rememer that we have seen dir.""" 351 self.seen[dir] = 1 352 353 def add(self, list, data, clue=''): 354 """add data to list if it isn't already there.""" 355 if data not in list: 356 list.append(data) 357 if self.debug: 358 print("%s: %sAdd: %s" % (self.name, clue, data), file=self.debug_out) 359 360 def find_top(self, path, list): 361 """the logical tree may be split across multiple trees""" 362 for top in list: 363 if path.startswith(top): 364 if self.debug > 2: 365 print("found in", top, file=self.debug_out) 366 return top 367 return None 368 369 def find_obj(self, objroot, dir, path, input): 370 """return path within objroot, taking care of .dirdep files""" 371 ddep = None 372 for ddepf in [path + '.dirdep', dir + '/.dirdep']: 373 if not ddep and os.path.exists(ddepf): 374 ddep = open(ddepf, 'r').readline().strip('# \n') 375 if self.debug > 1: 376 print("found %s: %s\n" % (ddepf, ddep), file=self.debug_out) 377 for e in self.exts: 378 if ddep.endswith(e): 379 ddep = ddep[0:-len(e)] 380 break 381 382 if not ddep: 383 # no .dirdeps, so remember that we've seen the raw input 384 self.seenit(input) 385 self.seenit(dir) 386 if self.machine == 'none': 387 if dir.startswith(objroot): 388 return dir.replace(objroot,'') 389 return None 390 m = self.dirdep_re.match(dir.replace(objroot,'')) 391 if m: 392 ddep = m.group(2) 393 dmachine = m.group(1) 394 if dmachine != self.machine: 395 if not (self.machine == 'host' and 396 dmachine == self.host_target): 397 if self.debug > 2: 398 print("adding .%s to %s" % (dmachine, ddep), file=self.debug_out) 399 ddep += '.' + dmachine 400 401 return ddep 402 403 def try_parse(self, name=None, file=None): 404 """give file and line number causing exception""" 405 try: 406 self.parse(name, file) 407 except: 408 # give a useful clue 409 print('{}:{}: '.format(self.name, self.line), end=' ', file=sys.stderr) 410 raise 411 412 def parse(self, name=None, file=None): 413 """A meta file looks like: 414 415 # Meta data file "path" 416 CMD "command-line" 417 CWD "cwd" 418 TARGET "target" 419 -- command output -- 420 -- filemon acquired metadata -- 421 # buildmon version 3 422 V 3 423 C "pid" "cwd" 424 E "pid" "path" 425 F "pid" "child" 426 R "pid" "path" 427 W "pid" "path" 428 X "pid" "status" 429 D "pid" "path" 430 L "pid" "src" "target" 431 M "pid" "old" "new" 432 S "pid" "path" 433 # Bye bye 434 435 We go to some effort to avoid processing a dependency more than once. 436 Of the above record types only C,E,F,L,R,V and W are of interest. 437 """ 438 439 version = 0 # unknown 440 if name: 441 self.name = name; 442 if file: 443 f = file 444 cwd = self.last_dir = self.cwd 445 else: 446 f = open(self.name, 'r') 447 skip = True 448 pid_cwd = {} 449 pid_last_dir = {} 450 last_pid = 0 451 eof_token = False 452 453 self.line = 0 454 if self.curdir: 455 self.seenit(self.curdir) # we ignore this 456 457 interesting = '#CEFLRVX' 458 for line in f: 459 self.line += 1 460 # ignore anything we don't care about 461 if not line[0] in interesting: 462 continue 463 if self.debug > 2: 464 print("input:", line, end=' ', file=self.debug_out) 465 w = line.split() 466 467 if skip: 468 if w[0] == 'V': 469 skip = False 470 version = int(w[1]) 471 """ 472 if version < 4: 473 # we cannot ignore 'W' records 474 # as they may be 'rw' 475 interesting += 'W' 476 """ 477 elif w[0] == 'CWD': 478 self.cwd = cwd = self.last_dir = w[1] 479 self.seenit(cwd) # ignore this 480 if self.debug: 481 print("%s: CWD=%s" % (self.name, cwd), file=self.debug_out) 482 continue 483 484 if w[0] == '#': 485 # check the file has not been truncated 486 if line.find('Bye') > 0: 487 eof_token = True 488 continue 489 490 pid = int(w[1]) 491 if pid != last_pid: 492 if last_pid: 493 pid_last_dir[last_pid] = self.last_dir 494 cwd = pid_cwd.get(pid, self.cwd) 495 self.last_dir = pid_last_dir.get(pid, self.cwd) 496 last_pid = pid 497 498 # process operations 499 if w[0] == 'F': 500 npid = int(w[2]) 501 pid_cwd[npid] = cwd 502 pid_last_dir[npid] = cwd 503 last_pid = npid 504 continue 505 elif w[0] == 'C': 506 cwd = abspath(w[2], cwd, None, self.debug, self.debug_out) 507 if not cwd: 508 cwd = w[2] 509 if self.debug > 1: 510 print("missing cwd=", cwd, file=self.debug_out) 511 if cwd.endswith('/.'): 512 cwd = cwd[0:-2] 513 self.last_dir = pid_last_dir[pid] = cwd 514 pid_cwd[pid] = cwd 515 if self.debug > 1: 516 print("cwd=", cwd, file=self.debug_out) 517 continue 518 519 if w[0] == 'X': 520 try: 521 del self.pids[pid] 522 except KeyError: 523 pass 524 continue 525 526 if w[2] in self.seen: 527 if self.debug > 2: 528 print("seen:", w[2], file=self.debug_out) 529 continue 530 # file operations 531 if w[0] in 'ML': 532 # these are special, tread src as read and 533 # target as write 534 self.parse_path(w[2].strip("'"), cwd, 'R', w) 535 self.parse_path(w[3].strip("'"), cwd, 'W', w) 536 continue 537 elif w[0] in 'ERWS': 538 path = w[2] 539 if w[0] == 'E': 540 self.pids[pid] = path 541 elif path == '.': 542 continue 543 self.parse_path(path, cwd, w[0], w) 544 545 if version == 0: 546 raise AssertionError('missing filemon data') 547 if not eof_token: 548 raise AssertionError('truncated filemon data') 549 550 setid_pids = [] 551 # self.pids should be empty! 552 for pid,path in self.pids.items(): 553 try: 554 # no guarantee that path is still valid 555 if os.stat(path).st_mode & (stat.S_ISUID|stat.S_ISGID): 556 # we do not expect anything after Exec 557 setid_pids.append(pid) 558 continue 559 except: 560 # we do not care why the above fails, 561 # we do not want to miss the ERROR below. 562 pass 563 print("ERROR: missing eXit for {} pid {}".format(path, pid)) 564 for pid in setid_pids: 565 del self.pids[pid] 566 assert(len(self.pids) == 0) 567 if not file: 568 f.close() 569 570 def is_src(self, base, dir, rdir): 571 """is base in srctop""" 572 for dir in [dir,rdir]: 573 if not dir: 574 continue 575 path = '/'.join([dir,base]) 576 srctop = self.find_top(path, self.srctops) 577 if srctop: 578 if self.dpdeps: 579 self.add(self.file_deps, path.replace(srctop,''), 'file') 580 self.add(self.src_deps, dir.replace(srctop,''), 'src') 581 self.seenit(dir) 582 return True 583 return False 584 585 def parse_path(self, path, cwd, op=None, w=[]): 586 """look at a path for the op specified""" 587 588 if not op: 589 op = w[0] 590 591 # we are never interested in .dirdep files as dependencies 592 if path.endswith('.dirdep'): 593 return 594 for p in self.excludes: 595 if p and path.startswith(p): 596 if self.debug > 2: 597 print("exclude:", p, path, file=self.debug_out) 598 return 599 # we don't want to resolve the last component if it is 600 # a symlink 601 path = resolve(path, cwd, self.last_dir, self.debug, self.debug_out) 602 if not path: 603 return 604 dir,base = os.path.split(path) 605 if dir in self.seen: 606 if self.debug > 2: 607 print("seen:", dir, file=self.debug_out) 608 return 609 # we can have a path in an objdir which is a link 610 # to the src dir, we may need to add dependencies for each 611 rdir = dir 612 dir = abspath(dir, cwd, self.last_dir, self.debug, self.debug_out) 613 if dir: 614 rdir = os.path.realpath(dir) 615 else: 616 dir = rdir 617 if rdir == dir: 618 rdir = None 619 # now put path back together 620 path = '/'.join([dir,base]) 621 if self.debug > 1: 622 print("raw=%s rdir=%s dir=%s path=%s" % (w[2], rdir, dir, path), file=self.debug_out) 623 if op in 'RWS': 624 if path in [self.last_dir, cwd, self.cwd, self.curdir]: 625 if self.debug > 1: 626 print("skipping:", path, file=self.debug_out) 627 return 628 if os.path.isdir(path): 629 if op in 'RW': 630 self.last_dir = path; 631 if self.debug > 1: 632 print("ldir=", self.last_dir, file=self.debug_out) 633 return 634 635 if op in 'ER': 636 # finally, we get down to it 637 if dir == self.cwd or dir == self.curdir: 638 return 639 if self.is_src(base, dir, rdir): 640 self.seenit(w[2]) 641 if not rdir: 642 return 643 644 objroot = None 645 for dir in [dir,rdir]: 646 if not dir: 647 continue 648 objroot = self.find_top(dir, self.objroots) 649 if objroot: 650 break 651 if objroot: 652 ddep = self.find_obj(objroot, dir, path, w[2]) 653 if ddep: 654 self.add(self.obj_deps, ddep, 'obj') 655 if self.dpdeps and objroot.endswith('/stage/'): 656 sp = '/'.join(path.replace(objroot,'').split('/')[1:]) 657 self.add(self.file_deps, sp, 'file') 658 else: 659 # don't waste time looking again 660 self.seenit(w[2]) 661 self.seenit(dir) 662 663 664def main(argv, klass=MetaFile, xopts='', xoptf=None): 665 """Simple driver for class MetaFile. 666 667 Usage: 668 script [options] [key=value ...] "meta" ... 669 670 Options and key=value pairs contribute to the 671 dictionary passed to MetaFile. 672 673 -S "SRCTOP" 674 add "SRCTOP" to the "SRCTOPS" list. 675 676 -C "CURDIR" 677 678 -O "OBJROOT" 679 add "OBJROOT" to the "OBJROOTS" list. 680 681 -m "MACHINE" 682 683 -a "MACHINE_ARCH" 684 685 -H "HOST_TARGET" 686 687 -D "DPDEPS" 688 689 -d bumps debug level 690 691 """ 692 import getopt 693 694 # import Psyco if we can 695 # it can speed things up quite a bit 696 have_psyco = 0 697 try: 698 import psyco 699 psyco.full() 700 have_psyco = 1 701 except: 702 pass 703 704 conf = { 705 'SRCTOPS': [], 706 'OBJROOTS': [], 707 'EXCLUDES': [], 708 } 709 710 try: 711 machine = os.environ['MACHINE'] 712 if machine: 713 conf['MACHINE'] = machine 714 machine_arch = os.environ['MACHINE_ARCH'] 715 if machine_arch: 716 conf['MACHINE_ARCH'] = machine_arch 717 srctop = os.environ['SB_SRC'] 718 if srctop: 719 conf['SRCTOPS'].append(srctop) 720 objroot = os.environ['SB_OBJROOT'] 721 if objroot: 722 conf['OBJROOTS'].append(objroot) 723 except: 724 pass 725 726 debug = 0 727 output = True 728 729 opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts) 730 for o, a in opts: 731 if o == '-a': 732 conf['MACHINE_ARCH'] = a 733 elif o == '-d': 734 debug += 1 735 elif o == '-q': 736 output = False 737 elif o == '-H': 738 conf['HOST_TARGET'] = a 739 elif o == '-S': 740 if a not in conf['SRCTOPS']: 741 conf['SRCTOPS'].append(a) 742 elif o == '-C': 743 conf['CURDIR'] = a 744 elif o == '-O': 745 if a not in conf['OBJROOTS']: 746 conf['OBJROOTS'].append(a) 747 elif o == '-R': 748 conf['RELDIR'] = a 749 elif o == '-D': 750 conf['DPDEPS'] = a 751 elif o == '-m': 752 conf['MACHINE'] = a 753 elif o == '-T': 754 conf['TARGET_SPEC'] = a 755 elif o == '-X': 756 if a not in conf['EXCLUDES']: 757 conf['EXCLUDES'].append(a) 758 elif xoptf: 759 xoptf(o, a, conf) 760 761 conf['debug'] = debug 762 763 # get any var=val assignments 764 eaten = [] 765 for a in args: 766 if a.find('=') > 0: 767 k,v = a.split('=') 768 if k in ['SRCTOP','OBJROOT','SRCTOPS','OBJROOTS']: 769 if k == 'SRCTOP': 770 k = 'SRCTOPS' 771 elif k == 'OBJROOT': 772 k = 'OBJROOTS' 773 if v not in conf[k]: 774 conf[k].append(v) 775 else: 776 conf[k] = v 777 eaten.append(a) 778 continue 779 break 780 781 for a in eaten: 782 args.remove(a) 783 784 debug_out = conf.get('debug_out', sys.stderr) 785 786 if debug: 787 print("config:", file=debug_out) 788 print("psyco=", have_psyco, file=debug_out) 789 for k,v in list(conf.items()): 790 print("%s=%s" % (k,v), file=debug_out) 791 792 m = None 793 for a in args: 794 if a.endswith('.meta'): 795 if not os.path.exists(a): 796 continue 797 m = klass(a, conf) 798 elif a.startswith('@'): 799 # there can actually multiple files per line 800 for line in open(a[1:]): 801 for f in line.strip().split(): 802 if not os.path.exists(f): 803 continue 804 m = klass(f, conf) 805 806 if output and m: 807 print(m.dirdeps()) 808 809 print(m.src_dirdeps('\nsrc:')) 810 811 dpdeps = conf.get('DPDEPS') 812 if dpdeps: 813 m.file_depends(open(dpdeps, 'w')) 814 815 return m 816 817if __name__ == '__main__': 818 try: 819 main(sys.argv) 820 except: 821 # yes, this goes to stdout 822 print("ERROR: ", sys.exc_info()[1]) 823 raise 824 825