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 34import getopt 35import os 36import stat 37import sys 38 39from pkg import actions 40from 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# 48OUTPUTMAP = { 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 73ALLMODECHECKS = frozenset(("m", "w", "s", "o")) 74DEFAULTMODECHECKS = frozenset(("m", "w", "o")) 75 76class 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 284class 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 327class 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 343class 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 394class 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 438class 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 447class 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 551class 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 565class 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 = action.get_variants() 639 if "variant.arch" in var and arch not in var["variant.arch"]: 640 return 641 642 self[path] = ActionInfo(action) 643 if modechecks is not None and path not in exceptions: 644 modewarnings.update(self[path].checkmodes(modechecks)) 645 646 if len(modewarnings) > 0: 647 print "warning: unsafe permissions in %s" % mfile 648 for w in sorted(modewarnings): 649 print w 650 print "" 651 652 def adddir(self, mdir, arch, modechecks, exceptions): 653 """Walks the specified directory looking for pkg(5) manifests. 654 """ 655 for mfile in os.listdir(mdir): 656 if (mfile.endswith(".mog") and 657 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)): 658 try: 659 self.addmanifest(mdir, mfile, arch, modechecks, exceptions) 660 except IOError, exc: 661 sys.stderr.write("warning: %s\n" % str(exc)) 662 663 def resolvehardlinks(self): 664 """Populates mode, group, and owner for resolved (ie link target 665 is present in the manifest tree) hard links. 666 """ 667 for info in self.values(): 668 if info.name() == "hardlink": 669 tgt = info.hardkey 670 if tgt in self: 671 tgtinfo = self[tgt] 672 info.owner = tgtinfo.owner 673 info.group = tgtinfo.group 674 info.mode = tgtinfo.mode 675 676class ExceptionList(set): 677 """Keep track of an exception list as a set of paths to be excluded 678 from any other lists we build. 679 """ 680 681 def __init__(self, files, arch): 682 set.__init__(self) 683 for fname in files: 684 try: 685 self.readexceptionfile(fname, arch) 686 except IOError, exc: 687 sys.stderr.write("warning: cannot read exception file: %s\n" % 688 str(exc)) 689 690 def readexceptionfile(self, efile, arch): 691 """Build a list of all pathnames from the specified file that 692 either apply to all architectures (ie which have no trailing 693 architecture tokens), or to the specified architecture (ie 694 which have the value of the arch arg as a trailing 695 architecture token.) 696 """ 697 698 excfile = open(efile) 699 700 for exc in excfile: 701 exc = exc.split() 702 if len(exc) and exc[0][0] != "#": 703 if arch in (exc[1:] or arch): 704 self.add(os.path.normpath(exc[0])) 705 706 excfile.close() 707 708 709USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2] 710 711where input_1 and input_2 may specify proto lists, proto areas, 712or manifest directories. For proto lists, use one or more 713 714 -l file 715 716arguments. For proto areas, use one or more 717 718 -p dir 719 720arguments. For manifest directories, use one or more 721 722 -m dir 723 724arguments. 725 726If -L or -M is specified, then only one input source is allowed, and 727it should be one or more manifest directories. These two options are 728mutually exclusive. 729 730The -L option is used to generate a proto list to stdout. 731 732The -M option is used to check for safe file and directory modes. 733By default, this causes all mode checks to be performed. Individual 734mode checks may be turned off using "-X check," where "check" comes 735from the following set of checks: 736 737 m check for group or other write permissions 738 w check for user write permissions on files and directories 739 not owned by root 740 s check for group/other read permission on executable files 741 that have setuid/setgid bit(s) 742 o check for files that could be safely owned by root 743""" % sys.argv[0] 744 745 746def usage(msg=None): 747 """Try to give the user useful information when they don't get the 748 command syntax right. 749 """ 750 if msg: 751 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) 752 sys.stderr.write(USAGE) 753 sys.exit(2) 754 755 756def main(argv): 757 """Compares two out of three possible data sources: a proto list, a 758 set of proto areas, and a set of manifests. 759 """ 760 try: 761 opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:') 762 except getopt.GetoptError, exc: 763 usage(str(exc)) 764 765 if args: 766 usage() 767 768 arch = None 769 exceptionlists = [] 770 listonly = False 771 manifestdirs = [] 772 manifesttree = ManifestTree("manifests") 773 protodirs = [] 774 prototree = ProtoTree("proto area") 775 protolists = [] 776 protolist = ProtoTree("proto list") 777 modechecks = set() 778 togglemodechecks = set() 779 trees = [] 780 comparing = set() 781 verbose = False 782 783 for opt, arg in opts: 784 if opt == "-a": 785 if arch: 786 usage("may only specify one architecture") 787 else: 788 arch = arg 789 elif opt == "-e": 790 exceptionlists.append(arg) 791 elif opt == "-L": 792 listonly = True 793 elif opt == "-l": 794 comparing.add("protolist") 795 protolists.append(os.path.normpath(arg)) 796 elif opt == "-M": 797 modechecks.update(DEFAULTMODECHECKS) 798 elif opt == "-m": 799 comparing.add("manifests") 800 manifestdirs.append(os.path.normpath(arg)) 801 elif opt == "-p": 802 comparing.add("proto area") 803 protodirs.append(os.path.normpath(arg)) 804 elif opt == "-v": 805 verbose = True 806 elif opt == "-X": 807 togglemodechecks.add(arg) 808 809 if listonly or len(modechecks) > 0: 810 if len(comparing) != 1 or "manifests" not in comparing: 811 usage("-L and -M require one or more -m args, and no -l or -p") 812 if listonly and len(modechecks) > 0: 813 usage("-L and -M are mutually exclusive") 814 elif len(comparing) != 2: 815 usage("must specify exactly two of -l, -m, and -p") 816 817 if len(togglemodechecks) > 0 and len(modechecks) == 0: 818 usage("-X requires -M") 819 820 for s in togglemodechecks: 821 if s not in ALLMODECHECKS: 822 usage("unknown mode check %s" % s) 823 modechecks.symmetric_difference_update((s)) 824 825 if len(modechecks) == 0: 826 modechecks = None 827 828 if not arch: 829 usage("must specify architecture") 830 831 exceptions = ExceptionList(exceptionlists, arch) 832 originalexceptions = exceptions.copy() 833 834 if len(manifestdirs) > 0: 835 for mdir in manifestdirs: 836 manifesttree.adddir(mdir, arch, modechecks, exceptions) 837 if listonly: 838 manifesttree.resolvehardlinks() 839 for info in manifesttree.values(): 840 print "%s" % info.protostr() 841 sys.exit(0) 842 if modechecks is not None: 843 sys.exit(0) 844 trees.append(manifesttree) 845 846 if len(protodirs) > 0: 847 for pdir in protodirs: 848 prototree.adddir(pdir, exceptions) 849 trees.append(prototree) 850 851 if len(protolists) > 0: 852 for plist in protolists: 853 try: 854 protolist.addprotolist(plist, exceptions) 855 except IOError, exc: 856 sys.stderr.write("warning: %s\n" % str(exc)) 857 trees.append(protolist) 858 859 if verbose and exceptions: 860 print "Entries present in exception list but missing from proto area:" 861 for exc in sorted(exceptions): 862 print "\t%s" % exc 863 print "" 864 865 usedexceptions = originalexceptions.difference(exceptions) 866 harmfulexceptions = usedexceptions.intersection(manifesttree) 867 if harmfulexceptions: 868 print "Entries present in exception list but also in manifests:" 869 for exc in sorted(harmfulexceptions): 870 print "\t%s" % exc 871 del manifesttree[exc] 872 print "" 873 874 trees[0].compare(trees[1]) 875 876if __name__ == '__main__': 877 try: 878 main(sys.argv[1:]) 879 except KeyboardInterrupt: 880 sys.exit(1) 881 except IOError: 882 sys.exit(1) 883