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