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