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