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