1 #!/usr/bin/python2.6 2 # 3 # CDDL HEADER START 4 # 5 # The contents of this file are subject to the terms of the 6 # Common Development and Distribution License (the "License"). 7 # You may not use this file except in compliance with the License. 8 # 9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10 # or http://www.opensolaris.org/os/licensing. 11 # See the License for the specific language governing permissions 12 # and limitations under the License. 13 # 14 # When distributing Covered Code, include this CDDL HEADER in each 15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16 # If applicable, add the following below this CDDL HEADER, with the 17 # fields enclosed by brackets "[]" replaced with your own identifying 18 # information: Portions Copyright [yyyy] [name of copyright owner] 19 # 20 # CDDL HEADER END 21 # 22 23 # 24 # Copyright 2010 Sun Microsystems, Inc. All rights reserved. 25 # Use is subject to license terms. 26 # 27 28 # 29 # Compare the content generated by a build to a set of manifests 30 # describing how that content is to be delivered. 31 # 32 33 34 import getopt 35 import os 36 import stat 37 import sys 38 39 from pkg import actions 40 from pkg import manifest 41 42 43 # 44 # Dictionary used to map action names to output format. Each entry is 45 # indexed by action name, and consists of a list of tuples that map 46 # FileInfo class members to output labels. 47 # 48 OUTPUTMAP = { 49 "dir": [ 50 ("group", "group="), 51 ("mode", "mode="), 52 ("owner", "owner="), 53 ("path", "path=") 54 ], 55 "file": [ 56 ("hash", ""), 57 ("group", "group="), 58 ("mode", "mode="), 59 ("owner", "owner="), 60 ("path", "path=") 61 ], 62 "link": [ 63 ("path", "path="), 64 ("target", "target=") 65 ], 66 "hardlink": [ 67 ("path", "path="), 68 ("hardkey", "target=") 69 ], 70 } 71 72 # Mode checks used to validate safe file and directory permissions 73 ALLMODECHECKS = frozenset(("m", "w", "s", "o")) 74 DEFAULTMODECHECKS = frozenset(("m", "w", "o")) 75 76 class FileInfo(object): 77 """Base class to represent a file. 78 79 Subclassed according to whether the file represents an actual filesystem 80 object (RealFileInfo) or an IPS manifest action (ActionInfo). 81 """ 82 83 def __init__(self): 84 self.path = None 85 self.isdir = False 86 self.target = None 87 self.owner = None 88 self.group = None 89 self.mode = None 90 self.hardkey = None 91 self.hardpaths = set() 92 self.editable = False 93 94 def name(self): 95 """Return the IPS action name of a FileInfo object. 96 """ 97 if self.isdir: 98 return "dir" 99 100 if self.target: 101 return "link" 102 103 if self.hardkey: 104 return "hardlink" 105 106 return "file" 107 108 def checkmodes(self, modechecks): 109 """Check for and report on unsafe permissions. 110 111 Returns a potentially empty list of warning strings. 112 """ 113 w = [] 114 115 t = self.name() 116 if t in ("link", "hardlink"): 117 return w 118 m = int(self.mode, 8) 119 o = self.owner 120 p = self.path 121 122 if "s" in modechecks and t == "file": 123 if m & (stat.S_ISUID | stat.S_ISGID): 124 if m & (stat.S_IRGRP | stat.S_IROTH): 125 w.extend(["%s: 0%o: setuid/setgid file should not be " \ 126 "readable by group or other" % (p, m)]) 127 128 if "o" in modechecks and o != "root" and ((m & stat.S_ISUID) == 0): 129 mu = (m & stat.S_IRWXU) >> 6 130 mg = (m & stat.S_IRWXG) >> 3 131 mo = m & stat.S_IRWXO 132 e = self.editable 133 134 if (((mu & 02) == 0 and (mo & mg & 04) == 04) or 135 (t == "file" and mo & 01 == 1) or 136 (mg, mo) == (mu, mu) or 137 ((t == "file" and not e or t == "dir" and o == "bin") and 138 (mg & 05 == mo & 05)) or 139 (t == "file" and o == "bin" and mu & 01 == 01) or 140 (m & 0105 != 0 and p.startswith("etc/security/dev/"))): 141 w.extend(["%s: owner \"%s\" may be safely " \ 142 "changed to \"root\"" % (p, o)]) 143 144 if "w" in modechecks and t == "file" and o != "root": 145 uwx = stat.S_IWUSR | stat.S_IXUSR 146 if m & uwx == uwx: 147 w.extend(["%s: non-root-owned executable should not " \ 148 "also be writable by owner." % p]) 149 150 if ("m" in modechecks and 151 m & (stat.S_IWGRP | stat.S_IWOTH) != 0 and 152 m & stat.S_ISVTX == 0): 153 w.extend(["%s: 0%o: should not be writable by group or other" % 154 (p, m)]) 155 156 return w 157 158 def __ne__(self, other): 159 """Compare two FileInfo objects. 160 161 Note this is the "not equal" comparison, so a return value of False 162 indicates that the objects are functionally equivalent. 163 """ 164 # 165 # Map the objects such that the lhs is always the ActionInfo, 166 # and the rhs is always the RealFileInfo. 167 # 168 # It's only really important that the rhs not be an 169 # ActionInfo; if we're comparing FileInfo the RealFileInfo, it 170 # won't actually matter what we choose. 171 # 172 if isinstance(self, ActionInfo): 173 lhs = self 174 rhs = other 175 else: 176 lhs = other 177 rhs = self 178 179 # 180 # Because the manifest may legitimately translate a relative 181 # path from the proto area into a different path on the installed 182 # system, we don't compare paths here. We only expect this comparison 183 # to be invoked on items with identical relative paths in 184 # first place. 185 # 186 187 # 188 # All comparisons depend on type. For symlink and directory, they 189 # must be the same. For file and hardlink, see below. 190 # 191 typelhs = lhs.name() 192 typerhs = rhs.name() 193 if typelhs in ("link", "dir"): 194 if typelhs != typerhs: 195 return True 196 197 # 198 # For symlinks, all that's left is the link target. 199 # 200 if typelhs == "link": 201 return lhs.target != rhs.target 202 203 # 204 # For a directory, it's important that both be directories, 205 # the modes be identical, and the paths are identical. We already 206 # checked all but the modes above. 207 # 208 # If both objects are files, then we're in the same boat. 209 # 210 if typelhs == "dir" or (typelhs == "file" and typerhs == "file"): 211 return lhs.mode != rhs.mode 212 213 # 214 # For files or hardlinks: 215 # 216 # Since the key space is different (inodes for real files and 217 # actual link targets for hard links), and since the proto area will 218 # identify all N occurrences as hardlinks, but the manifests as one 219 # file and N-1 hardlinks, we have to compare files to hardlinks. 220 # 221 222 # 223 # If they're both hardlinks, we just make sure that 224 # the same target path appears in both sets of 225 # possible targets. 226 # 227 if typelhs == "hardlink" and typerhs == "hardlink": 228 return len(lhs.hardpaths.intersection(rhs.hardpaths)) == 0 229 230 # 231 # Otherwise, we have a mix of file and hardlink, so we 232 # need to make sure that the file path appears in the 233 # set of possible target paths for the hardlink. 234 # 235 # We already know that the ActionInfo, if present, is the lhs 236 # operator. So it's the rhs operator that's guaranteed to 237 # have a set of hardpaths. 238 # 239 return lhs.path not in rhs.hardpaths 240 241 def __str__(self): 242 """Return an action-style representation of a FileInfo object. 243 244 We don't currently quote items with embedded spaces. If we 245 ever decide to parse this output, we'll want to revisit that. 246 """ 247 name = self.name() 248 out = name 249 250 for member, label in OUTPUTMAP[name]: 251 out += " " + label + str(getattr(self, member)) 252 253 return out 254 255 def protostr(self): 256 """Return a protolist-style representation of a FileInfo object. 257 """ 258 target = "-" 259 major = "-" 260 minor = "-" 261 262 mode = self.mode 263 owner = self.owner 264 group = self.group 265 266 name = self.name() 267 if name == "dir": 268 ftype = "d" 269 elif name in ("file", "hardlink"): 270 ftype = "f" 271 elif name == "link": 272 ftype = "s" 273 target = self.target 274 mode = "777" 275 owner = "root" 276 group = "other" 277 278 out = "%c %-30s %-20s %4s %-5s %-5s %6d %2ld - -" % \ 279 (ftype, self.path, target, mode, owner, group, 0, 1) 280 281 return out 282 283 284 class ActionInfo(FileInfo): 285 """Object to track information about manifest actions. 286 287 This currently understands file, link, dir, and hardlink actions. 288 """ 289 290 def __init__(self, action): 291 FileInfo.__init__(self) 292 # 293 # Currently, all actions that we support have a "path" 294 # attribute. If that changes, then we'll need to 295 # catch a KeyError from this assignment. 296 # 297 self.path = action.attrs["path"] 298 299 if action.name == "file": 300 self.owner = action.attrs["owner"] 301 self.group = action.attrs["group"] 302 self.mode = action.attrs["mode"] 303 self.hash = action.hash 304 if "preserve" in action.attrs: 305 self.editable = True 306 elif action.name == "link": 307 target = action.attrs["target"] 308 self.target = os.path.normpath(target) 309 elif action.name == "dir": 310 self.owner = action.attrs["owner"] 311 self.group = action.attrs["group"] 312 self.mode = action.attrs["mode"] 313 self.isdir = True 314 elif action.name == "hardlink": 315 target = os.path.normpath(action.get_target_path()) 316 self.hardkey = target 317 self.hardpaths.add(target) 318 319 @staticmethod 320 def supported(action): 321 """Indicates whether the specified IPS action time is 322 correctly handled by the ActionInfo constructor. 323 """ 324 return action in frozenset(("file", "dir", "link", "hardlink")) 325 326 327 class UnsupportedFileFormatError(Exception): 328 """This means that the stat.S_IFMT returned something we don't 329 support, ie a pipe or socket. If it's appropriate for such an 330 object to be in the proto area, then the RealFileInfo constructor 331 will need to evolve to support it, or it will need to be in the 332 exception list. 333 """ 334 def __init__(self, path, mode): 335 Exception.__init__(self) 336 self.path = path 337 self.mode = mode 338 339 def __str__(self): 340 return '%s: unsupported S_IFMT %07o' % (self.path, self.mode) 341 342 343 class RealFileInfo(FileInfo): 344 """Object to track important-to-packaging file information. 345 346 This currently handles regular files, directories, and symbolic links. 347 348 For multiple RealFileInfo objects with identical hardkeys, there 349 is no way to determine which of the hard links should be 350 delivered as a file, and which as hardlinks. 351 """ 352 353 def __init__(self, root=None, path=None): 354 FileInfo.__init__(self) 355 self.path = path 356 path = os.path.join(root, path) 357 lstat = os.lstat(path) 358 mode = lstat.st_mode 359 360 # 361 # Per stat.py, these cases are mutually exclusive. 362 # 363 if stat.S_ISREG(mode): 364 self.hash = self.path 365 elif stat.S_ISDIR(mode): 366 self.isdir = True 367 elif stat.S_ISLNK(mode): 368 self.target = os.path.normpath(os.readlink(path)) 369 else: 370 raise UnsupportedFileFormatError(path, mode) 371 372 if not stat.S_ISLNK(mode): 373 self.mode = "%04o" % stat.S_IMODE(mode) 374 # 375 # Instead of reading the group and owner from the proto area after 376 # a non-root build, just drop in dummy values. Since we don't 377 # compare them anywhere, this should allow at least marginally 378 # useful comparisons of protolist-style output. 379 # 380 self.owner = "owner" 381 self.group = "group" 382 383 # 384 # refcount > 1 indicates a hard link 385 # 386 if lstat.st_nlink > 1: 387 # 388 # This could get ugly if multiple proto areas reside 389 # on different filesystems. 390 # 391 self.hardkey = lstat.st_ino 392 393 394 class DirectoryTree(dict): 395 """Meant to be subclassed according to population method. 396 """ 397 def __init__(self, name): 398 dict.__init__(self) 399 self.name = name 400 401 def compare(self, other): 402 """Compare two different sets of FileInfo objects. 403 """ 404 keys1 = frozenset(self.keys()) 405 keys2 = frozenset(other.keys()) 406 407 common = keys1.intersection(keys2) 408 onlykeys1 = keys1.difference(common) 409 onlykeys2 = keys2.difference(common) 410 411 if onlykeys1: 412 print "Entries present in %s but not %s:" % \ 413 (self.name, other.name) 414 for path in sorted(onlykeys1): 415 print("\t%s" % str(self[path])) 416 print "" 417 418 if onlykeys2: 419 print "Entries present in %s but not %s:" % \ 420 (other.name, self.name) 421 for path in sorted(onlykeys2): 422 print("\t%s" % str(other[path])) 423 print "" 424 425 nodifferences = True 426 for path in sorted(common): 427 if self[path] != other[path]: 428 if nodifferences: 429 nodifferences = False 430 print "Entries that differ between %s and %s:" \ 431 % (self.name, other.name) 432 print("%14s %s" % (self.name, self[path])) 433 print("%14s %s" % (other.name, other[path])) 434 if not nodifferences: 435 print "" 436 437 438 class BadProtolistFormat(Exception): 439 """This means that the user supplied a file via -l, but at least 440 one line from that file doesn't have the right number of fields to 441 parse as protolist output. 442 """ 443 def __str__(self): 444 return 'bad proto list entry: "%s"' % Exception.__str__(self) 445 446 447 class ProtoTree(DirectoryTree): 448 """Describes one or more proto directories as a dictionary of 449 RealFileInfo objects, indexed by relative path. 450 """ 451 452 def adddir(self, proto, exceptions): 453 """Extends the ProtoTree dictionary with RealFileInfo 454 objects describing the proto dir, indexed by relative 455 path. 456 """ 457 newentries = {} 458 459 pdir = os.path.normpath(proto) 460 strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:] 461 for root, dirs, files in os.walk(pdir): 462 for name in dirs + files: 463 path = strippdir(root, name) 464 if path not in exceptions: 465 try: 466 newentries[path] = RealFileInfo(pdir, path) 467 except OSError, e: 468 sys.stderr.write("Warning: unable to stat %s: %s\n" % 469 (path, e)) 470 continue 471 else: 472 exceptions.remove(path) 473 if name in dirs: 474 dirs.remove(name) 475 476 # 477 # Find the sets of paths in this proto dir that are hardlinks 478 # to the same inode. 479 # 480 # It seems wasteful to store this in each FileInfo, but we 481 # otherwise need a linking mechanism. With this information 482 # here, FileInfo object comparison can be self contained. 483 # 484 # We limit this aggregation to a single proto dir, as 485 # represented by newentries. That means we don't need to care 486 # about proto dirs on separate filesystems, or about hardlinks 487 # that cross proto dir boundaries. 488 # 489 hk2path = {} 490 for path, fileinfo in newentries.iteritems(): 491 if fileinfo.hardkey: 492 hk2path.setdefault(fileinfo.hardkey, set()).add(path) 493 for fileinfo in newentries.itervalues(): 494 if fileinfo.hardkey: 495 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey]) 496 self.update(newentries) 497 498 def addprotolist(self, protolist, exceptions): 499 """Read in the specified file, assumed to be the 500 output of protolist. 501 502 This has been tested minimally, and is potentially useful for 503 comparing across the transition period, but should ultimately 504 go away. 505 """ 506 507 try: 508 plist = open(protolist) 509 except IOError, exc: 510 raise IOError("cannot open proto list: %s" % str(exc)) 511 512 newentries = {} 513 514 for pline in plist: 515 pline = pline.split() 516 # 517 # Use a FileInfo() object instead of a RealFileInfo() 518 # object because we want to avoid the RealFileInfo 519 # constructor, because there's nothing to actually stat(). 520 # 521 fileinfo = FileInfo() 522 try: 523 if pline[1] in exceptions: 524 exceptions.remove(pline[1]) 525 continue 526 if pline[0] == "d": 527 fileinfo.isdir = True 528 fileinfo.path = pline[1] 529 if pline[2] != "-": 530 fileinfo.target = os.path.normpath(pline[2]) 531 fileinfo.mode = int("0%s" % pline[3]) 532 fileinfo.owner = pline[4] 533 fileinfo.group = pline[5] 534 if pline[6] != "0": 535 fileinfo.hardkey = pline[6] 536 newentries[pline[1]] = fileinfo 537 except IndexError: 538 raise BadProtolistFormat(pline) 539 540 plist.close() 541 hk2path = {} 542 for path, fileinfo in newentries.iteritems(): 543 if fileinfo.hardkey: 544 hk2path.setdefault(fileinfo.hardkey, set()).add(path) 545 for fileinfo in newentries.itervalues(): 546 if fileinfo.hardkey: 547 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey]) 548 self.update(newentries) 549 550 551 class ManifestParsingError(Exception): 552 """This means that the Manifest.set_content() raised an 553 ActionError. We raise this, instead, to tell us which manifest 554 could not be parsed, rather than what action error we hit. 555 """ 556 def __init__(self, mfile, error): 557 Exception.__init__(self) 558 self.mfile = mfile 559 self.error = error 560 561 def __str__(self): 562 return "unable to parse manifest %s: %s" % (self.mfile, self.error) 563 564 565 class ManifestTree(DirectoryTree): 566 """Describes one or more directories containing arbitrarily 567 many manifests as a dictionary of ActionInfo objects, indexed 568 by the relative path of the data source within the proto area. 569 That path may or may not be the same as the path attribute of the 570 given action. 571 """ 572 573 def addmanifest(self, root, mfile, arch, modechecks, exceptions): 574 """Treats the specified input file as a pkg(5) package 575 manifest, and extends the ManifestTree dictionary with entries 576 for the actions therein. 577 """ 578 mfest = manifest.Manifest() 579 try: 580 mfest.set_content(open(os.path.join(root, mfile)).read()) 581 except IOError, exc: 582 raise IOError("cannot read manifest: %s" % str(exc)) 583 except actions.ActionError, exc: 584 raise ManifestParsingError(mfile, str(exc)) 585 586 # 587 # Make sure the manifest is applicable to the user-specified 588 # architecture. Assumption: if variant.arch is not an 589 # attribute of the manifest, then the package should be 590 # installed on all architectures. 591 # 592 if arch not in mfest.attributes.get("variant.arch", (arch,)): 593 return 594 595 modewarnings = set() 596 for action in mfest.gen_actions(): 597 if "path" not in action.attrs or \ 598 not ActionInfo.supported(action.name): 599 continue 600 601 # 602 # The dir action is currently fully specified, in that it 603 # lists owner, group, and mode attributes. If that 604 # changes in pkg(5) code, we'll need to revisit either this 605 # code or the ActionInfo() constructor. It's possible 606 # that the pkg(5) system could be extended to provide a 607 # mechanism for specifying directory permissions outside 608 # of the individual manifests that deliver files into 609 # those directories. Doing so at time of manifest 610 # processing would mean that validate_pkg continues to work, 611 # but doing so at time of publication would require updates. 612 # 613 614 # 615 # See pkgsend(1) for the use of NOHASH for objects with 616 # datastreams. Currently, that means "files," but this 617 # should work for any other such actions. 618 # 619 if getattr(action, "hash", "NOHASH") != "NOHASH": 620 path = action.hash 621 else: 622 path = action.attrs["path"] 623 624 # 625 # This is the wrong tool in which to enforce consistency 626 # on a set of manifests. So instead of comparing the 627 # different actions with the same "path" attribute, we 628 # use the first one. 629 # 630 if path in self: 631 continue 632 633 # 634 # As with the manifest itself, if an action has specified 635 # variant.arch, we look for the target architecture 636 # therein. 637 # 638 var = None 639 640 # 641 # The name of this method changed in pkg(5) build 150, we need to 642 # work with both sets. 643 # 644 if hasattr(action, 'get_variants'): 645 var = action.get_variants() 646 else: 647 var = action.get_variant_template() 648 if "variant.arch" in var and arch not in var["variant.arch"]: 649 return 650 651 self[path] = ActionInfo(action) 652 if modechecks is not None and path not in exceptions: 653 modewarnings.update(self[path].checkmodes(modechecks)) 654 655 if len(modewarnings) > 0: 656 print "warning: unsafe permissions in %s" % mfile 657 for w in sorted(modewarnings): 658 print w 659 print "" 660 661 def adddir(self, mdir, arch, modechecks, exceptions): 662 """Walks the specified directory looking for pkg(5) manifests. 663 """ 664 for mfile in os.listdir(mdir): 665 if (mfile.endswith(".mog") and 666 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)): 667 try: 668 self.addmanifest(mdir, mfile, arch, modechecks, exceptions) 669 except IOError, exc: 670 sys.stderr.write("warning: %s\n" % str(exc)) 671 672 def resolvehardlinks(self): 673 """Populates mode, group, and owner for resolved (ie link target 674 is present in the manifest tree) hard links. 675 """ 676 for info in self.values(): 677 if info.name() == "hardlink": 678 tgt = info.hardkey 679 if tgt in self: 680 tgtinfo = self[tgt] 681 info.owner = tgtinfo.owner 682 info.group = tgtinfo.group 683 info.mode = tgtinfo.mode 684 685 class ExceptionList(set): 686 """Keep track of an exception list as a set of paths to be excluded 687 from any other lists we build. 688 """ 689 690 def __init__(self, files, arch): 691 set.__init__(self) 692 for fname in files: 693 try: 694 self.readexceptionfile(fname, arch) 695 except IOError, exc: 696 sys.stderr.write("warning: cannot read exception file: %s\n" % 697 str(exc)) 698 699 def readexceptionfile(self, efile, arch): 700 """Build a list of all pathnames from the specified file that 701 either apply to all architectures (ie which have no trailing 702 architecture tokens), or to the specified architecture (ie 703 which have the value of the arch arg as a trailing 704 architecture token.) 705 """ 706 707 excfile = open(efile) 708 709 for exc in excfile: 710 exc = exc.split() 711 if len(exc) and exc[0][0] != "#": 712 if arch in (exc[1:] or arch): 713 self.add(os.path.normpath(exc[0])) 714 715 excfile.close() 716 717 718 USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2] 719 720 where input_1 and input_2 may specify proto lists, proto areas, 721 or manifest directories. For proto lists, use one or more 722 723 -l file 724 725 arguments. For proto areas, use one or more 726 727 -p dir 728 729 arguments. For manifest directories, use one or more 730 731 -m dir 732 733 arguments. 734 735 If -L or -M is specified, then only one input source is allowed, and 736 it should be one or more manifest directories. These two options are 737 mutually exclusive. 738 739 The -L option is used to generate a proto list to stdout. 740 741 The -M option is used to check for safe file and directory modes. 742 By default, this causes all mode checks to be performed. Individual 743 mode checks may be turned off using "-X check," where "check" comes 744 from the following set of checks: 745 746 m check for group or other write permissions 747 w check for user write permissions on files and directories 748 not owned by root 749 s check for group/other read permission on executable files 750 that have setuid/setgid bit(s) 751 o check for files that could be safely owned by root 752 """ % sys.argv[0] 753 754 755 def usage(msg=None): 756 """Try to give the user useful information when they don't get the 757 command syntax right. 758 """ 759 if msg: 760 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) 761 sys.stderr.write(USAGE) 762 sys.exit(2) 763 764 765 def main(argv): 766 """Compares two out of three possible data sources: a proto list, a 767 set of proto areas, and a set of manifests. 768 """ 769 try: 770 opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:') 771 except getopt.GetoptError, exc: 772 usage(str(exc)) 773 774 if args: 775 usage() 776 777 arch = None 778 exceptionlists = [] 779 listonly = False 780 manifestdirs = [] 781 manifesttree = ManifestTree("manifests") 782 protodirs = [] 783 prototree = ProtoTree("proto area") 784 protolists = [] 785 protolist = ProtoTree("proto list") 786 modechecks = set() 787 togglemodechecks = set() 788 trees = [] 789 comparing = set() 790 verbose = False 791 792 for opt, arg in opts: 793 if opt == "-a": 794 if arch: 795 usage("may only specify one architecture") 796 else: 797 arch = arg 798 elif opt == "-e": 799 exceptionlists.append(arg) 800 elif opt == "-L": 801 listonly = True 802 elif opt == "-l": 803 comparing.add("protolist") 804 protolists.append(os.path.normpath(arg)) 805 elif opt == "-M": 806 modechecks.update(DEFAULTMODECHECKS) 807 elif opt == "-m": 808 comparing.add("manifests") 809 manifestdirs.append(os.path.normpath(arg)) 810 elif opt == "-p": 811 comparing.add("proto area") 812 protodirs.append(os.path.normpath(arg)) 813 elif opt == "-v": 814 verbose = True 815 elif opt == "-X": 816 togglemodechecks.add(arg) 817 818 if listonly or len(modechecks) > 0: 819 if len(comparing) != 1 or "manifests" not in comparing: 820 usage("-L and -M require one or more -m args, and no -l or -p") 821 if listonly and len(modechecks) > 0: 822 usage("-L and -M are mutually exclusive") 823 elif len(comparing) != 2: 824 usage("must specify exactly two of -l, -m, and -p") 825 826 if len(togglemodechecks) > 0 and len(modechecks) == 0: 827 usage("-X requires -M") 828 829 for s in togglemodechecks: 830 if s not in ALLMODECHECKS: 831 usage("unknown mode check %s" % s) 832 modechecks.symmetric_difference_update((s)) 833 834 if len(modechecks) == 0: 835 modechecks = None 836 837 if not arch: 838 usage("must specify architecture") 839 840 exceptions = ExceptionList(exceptionlists, arch) 841 originalexceptions = exceptions.copy() 842 843 if len(manifestdirs) > 0: 844 for mdir in manifestdirs: 845 manifesttree.adddir(mdir, arch, modechecks, exceptions) 846 if listonly: 847 manifesttree.resolvehardlinks() 848 for info in manifesttree.values(): 849 print "%s" % info.protostr() 850 sys.exit(0) 851 if modechecks is not None: 852 sys.exit(0) 853 trees.append(manifesttree) 854 855 if len(protodirs) > 0: 856 for pdir in protodirs: 857 prototree.adddir(pdir, exceptions) 858 trees.append(prototree) 859 860 if len(protolists) > 0: 861 for plist in protolists: 862 try: 863 protolist.addprotolist(plist, exceptions) 864 except IOError, exc: 865 sys.stderr.write("warning: %s\n" % str(exc)) 866 trees.append(protolist) 867 868 if verbose and exceptions: 869 print "Entries present in exception list but missing from proto area:" 870 for exc in sorted(exceptions): 871 print "\t%s" % exc 872 print "" 873 874 usedexceptions = originalexceptions.difference(exceptions) 875 harmfulexceptions = usedexceptions.intersection(manifesttree) 876 if harmfulexceptions: 877 print "Entries present in exception list but also in manifests:" 878 for exc in sorted(harmfulexceptions): 879 print "\t%s" % exc 880 del manifesttree[exc] 881 print "" 882 883 trees[0].compare(trees[1]) 884 885 if __name__ == '__main__': 886 try: 887 main(sys.argv[1:]) 888 except KeyboardInterrupt: 889 sys.exit(1) 890 except IOError: 891 sys.exit(1)