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 288 289class ActionInfo(FileInfo): 290 """Object to track information about manifest actions. 291 292 This currently understands file, link, dir, and hardlink actions. 293 """ 294 295 def __init__(self, action): 296 FileInfo.__init__(self) 297 # 298 # Currently, all actions that we support have a "path" 299 # attribute. If that changes, then we'll need to 300 # catch a KeyError from this assignment. 301 # 302 self.path = action.attrs["path"] 303 304 if action.name == "file": 305 self.owner = action.attrs["owner"] 306 self.group = action.attrs["group"] 307 self.mode = action.attrs["mode"] 308 self.hash = action.hash 309 if "preserve" in action.attrs: 310 self.editable = True 311 elif action.name == "link": 312 target = action.attrs["target"] 313 self.target = os.path.normpath(target) 314 self.mediator = action.attrs.get("mediator") 315 elif action.name == "dir": 316 self.owner = action.attrs["owner"] 317 self.group = action.attrs["group"] 318 self.mode = action.attrs["mode"] 319 self.isdir = True 320 elif action.name == "hardlink": 321 target = os.path.normpath(action.get_target_path()) 322 self.hardkey = target 323 self.hardpaths.add(target) 324 325 @staticmethod 326 def supported(action): 327 """Indicates whether the specified IPS action time is 328 correctly handled by the ActionInfo constructor. 329 """ 330 return action in frozenset(("file", "dir", "link", "hardlink")) 331 332 333class UnsupportedFileFormatError(Exception): 334 """This means that the stat.S_IFMT returned something we don't 335 support, ie a pipe or socket. If it's appropriate for such an 336 object to be in the proto area, then the RealFileInfo constructor 337 will need to evolve to support it, or it will need to be in the 338 exception list. 339 """ 340 def __init__(self, path, mode): 341 Exception.__init__(self) 342 self.path = path 343 self.mode = mode 344 345 def __str__(self): 346 return '%s: unsupported S_IFMT %07o' % (self.path, self.mode) 347 348 349class RealFileInfo(FileInfo): 350 """Object to track important-to-packaging file information. 351 352 This currently handles regular files, directories, and symbolic links. 353 354 For multiple RealFileInfo objects with identical hardkeys, there 355 is no way to determine which of the hard links should be 356 delivered as a file, and which as hardlinks. 357 """ 358 359 def __init__(self, root=None, path=None): 360 FileInfo.__init__(self) 361 self.path = path 362 path = os.path.join(root, path) 363 lstat = os.lstat(path) 364 mode = lstat.st_mode 365 366 # 367 # Per stat.py, these cases are mutually exclusive. 368 # 369 if stat.S_ISREG(mode): 370 self.hash = self.path 371 elif stat.S_ISDIR(mode): 372 self.isdir = True 373 elif stat.S_ISLNK(mode): 374 self.target = os.path.normpath(os.readlink(path)) 375 self.mediator = None 376 else: 377 raise UnsupportedFileFormatError(path, mode) 378 379 if not stat.S_ISLNK(mode): 380 self.mode = "%04o" % stat.S_IMODE(mode) 381 # 382 # Instead of reading the group and owner from the proto area after 383 # a non-root build, just drop in dummy values. Since we don't 384 # compare them anywhere, this should allow at least marginally 385 # useful comparisons of protolist-style output. 386 # 387 self.owner = "owner" 388 self.group = "group" 389 390 # 391 # refcount > 1 indicates a hard link 392 # 393 if lstat.st_nlink > 1: 394 # 395 # This could get ugly if multiple proto areas reside 396 # on different filesystems. 397 # 398 self.hardkey = lstat.st_ino 399 400 401class DirectoryTree(dict): 402 """Meant to be subclassed according to population method. 403 """ 404 def __init__(self, name): 405 dict.__init__(self) 406 self.name = name 407 408 def compare(self, other): 409 """Compare two different sets of FileInfo objects. 410 """ 411 keys1 = frozenset(list(self.keys())) 412 keys2 = frozenset(list(other.keys())) 413 414 common = keys1.intersection(keys2) 415 onlykeys1 = keys1.difference(common) 416 onlykeys2 = keys2.difference(common) 417 418 if onlykeys1: 419 print("Entries present in %s but not %s:" % \ 420 (self.name, other.name)) 421 for path in sorted(onlykeys1): 422 print(("\t%s" % str(self[path]))) 423 print("") 424 425 if onlykeys2: 426 print("Entries present in %s but not %s:" % \ 427 (other.name, self.name)) 428 for path in sorted(onlykeys2): 429 print(("\t%s" % str(other[path]))) 430 print("") 431 432 nodifferences = True 433 for path in sorted(common): 434 if self[path] != other[path]: 435 if nodifferences: 436 nodifferences = False 437 print("Entries that differ between %s and %s:" \ 438 % (self.name, other.name)) 439 print(("%14s %s" % (self.name, self[path]))) 440 print(("%14s %s" % (other.name, other[path]))) 441 if not nodifferences: 442 print("") 443 444 445class BadProtolistFormat(Exception): 446 """This means that the user supplied a file via -l, but at least 447 one line from that file doesn't have the right number of fields to 448 parse as protolist output. 449 """ 450 def __str__(self): 451 return 'bad proto list entry: "%s"' % Exception.__str__(self) 452 453 454class ProtoTree(DirectoryTree): 455 """Describes one or more proto directories as a dictionary of 456 RealFileInfo objects, indexed by relative path. 457 """ 458 459 def adddir(self, proto, exceptions): 460 """Extends the ProtoTree dictionary with RealFileInfo 461 objects describing the proto dir, indexed by relative 462 path. 463 """ 464 newentries = {} 465 466 pdir = os.path.normpath(proto) 467 strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:] 468 for root, dirs, files in os.walk(pdir): 469 for name in dirs + files: 470 path = strippdir(root, name) 471 if path not in exceptions: 472 try: 473 newentries[path] = RealFileInfo(pdir, path) 474 except OSError as e: 475 sys.stderr.write("Warning: unable to stat %s: %s\n" % 476 (path, e)) 477 continue 478 else: 479 exceptions.remove(path) 480 if name in dirs: 481 dirs.remove(name) 482 483 # 484 # Find the sets of paths in this proto dir that are hardlinks 485 # to the same inode. 486 # 487 # It seems wasteful to store this in each FileInfo, but we 488 # otherwise need a linking mechanism. With this information 489 # here, FileInfo object comparison can be self contained. 490 # 491 # We limit this aggregation to a single proto dir, as 492 # represented by newentries. That means we don't need to care 493 # about proto dirs on separate filesystems, or about hardlinks 494 # that cross proto dir boundaries. 495 # 496 hk2path = {} 497 for path, fileinfo in newentries.items(): 498 if fileinfo.hardkey: 499 hk2path.setdefault(fileinfo.hardkey, set()).add(path) 500 for fileinfo in newentries.values(): 501 if fileinfo.hardkey: 502 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey]) 503 self.update(newentries) 504 505 def addprotolist(self, protolist, exceptions): 506 """Read in the specified file, assumed to be the 507 output of protolist. 508 509 This has been tested minimally, and is potentially useful for 510 comparing across the transition period, but should ultimately 511 go away. 512 """ 513 514 try: 515 plist = open(protolist) 516 except IOError as exc: 517 raise IOError("cannot open proto list: %s" % str(exc)) 518 519 newentries = {} 520 521 for pline in plist: 522 pline = pline.split() 523 # 524 # Use a FileInfo() object instead of a RealFileInfo() 525 # object because we want to avoid the RealFileInfo 526 # constructor, because there's nothing to actually stat(). 527 # 528 fileinfo = FileInfo() 529 try: 530 if pline[1] in exceptions: 531 exceptions.remove(pline[1]) 532 continue 533 if pline[0] == "d": 534 fileinfo.isdir = True 535 fileinfo.path = pline[1] 536 if pline[2] != "-": 537 fileinfo.target = os.path.normpath(pline[2]) 538 fileinfo.mode = int("0%s" % pline[3]) 539 fileinfo.owner = pline[4] 540 fileinfo.group = pline[5] 541 if pline[6] != "0": 542 fileinfo.hardkey = pline[6] 543 newentries[pline[1]] = fileinfo 544 except IndexError: 545 raise BadProtolistFormat(pline) 546 547 plist.close() 548 hk2path = {} 549 for path, fileinfo in newentries.items(): 550 if fileinfo.hardkey: 551 hk2path.setdefault(fileinfo.hardkey, set()).add(path) 552 for fileinfo in newentries.values(): 553 if fileinfo.hardkey: 554 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey]) 555 self.update(newentries) 556 557 558class ManifestParsingError(Exception): 559 """This means that the Manifest.set_content() raised an 560 ActionError. We raise this, instead, to tell us which manifest 561 could not be parsed, rather than what action error we hit. 562 """ 563 def __init__(self, mfile, error): 564 Exception.__init__(self) 565 self.mfile = mfile 566 self.error = error 567 568 def __str__(self): 569 return "unable to parse manifest %s: %s" % (self.mfile, self.error) 570 571 572class ManifestTree(DirectoryTree): 573 """Describes one or more directories containing arbitrarily 574 many manifests as a dictionary of ActionInfo objects, indexed 575 by the relative path of the data source within the proto area. 576 That path may or may not be the same as the path attribute of the 577 given action. 578 """ 579 580 def addmanifest(self, root, mfile, arch, modechecks, exceptions): 581 """Treats the specified input file as a pkg(7) package 582 manifest, and extends the ManifestTree dictionary with entries 583 for the actions therein. 584 """ 585 mfest = manifest.Manifest() 586 try: 587 mfest.set_content(open(os.path.join(root, mfile)).read()) 588 except IOError as exc: 589 raise IOError("cannot read manifest: %s" % str(exc)) 590 except actions.ActionError as exc: 591 raise ManifestParsingError(mfile, str(exc)) 592 593 # 594 # Make sure the manifest is applicable to the user-specified 595 # architecture. Assumption: if variant.arch is not an 596 # attribute of the manifest, then the package should be 597 # installed on all architectures. 598 # 599 if arch not in mfest.attributes.get("variant.arch", (arch,)): 600 return 601 602 modewarnings = set() 603 for action in mfest.gen_actions(): 604 if "path" not in action.attrs or \ 605 not ActionInfo.supported(action.name): 606 continue 607 608 # 609 # The dir action is currently fully specified, in that it 610 # lists owner, group, and mode attributes. If that 611 # changes in pkg(7) code, we'll need to revisit either this 612 # code or the ActionInfo() constructor. It's possible 613 # that the pkg(7) system could be extended to provide a 614 # mechanism for specifying directory permissions outside 615 # of the individual manifests that deliver files into 616 # those directories. Doing so at time of manifest 617 # processing would mean that validate_pkg continues to work, 618 # but doing so at time of publication would require updates. 619 # 620 621 # 622 # See pkgsend(1) for the use of NOHASH for objects with 623 # datastreams. Currently, that means "files," but this 624 # should work for any other such actions. 625 # 626 if getattr(action, "hash", "NOHASH") != "NOHASH": 627 path = action.hash 628 else: 629 path = action.attrs["path"] 630 631 # 632 # This is the wrong tool in which to enforce consistency 633 # on a set of manifests. So instead of comparing the 634 # different actions with the same "path" attribute, we 635 # use the first one. 636 # 637 if path in self: 638 continue 639 640 # 641 # As with the manifest itself, if an action has specified 642 # variant.arch, we look for the target architecture 643 # therein. 644 # 645 var = None 646 647 # 648 # The name of this method changed in pkg(7) build 150, we need to 649 # work with both sets. 650 # 651 if hasattr(action, 'get_variants'): 652 var = action.get_variants() 653 else: 654 var = action.get_variant_template() 655 if "variant.arch" in var and arch not in var["variant.arch"]: 656 return 657 658 self[path] = ActionInfo(action) 659 if modechecks is not None and path not in exceptions: 660 modewarnings.update(self[path].checkmodes(modechecks)) 661 662 if len(modewarnings) > 0: 663 print("warning: unsafe permissions in %s" % mfile) 664 for w in sorted(modewarnings): 665 print(w) 666 print("") 667 668 def adddir(self, mdir, arch, modechecks, exceptions): 669 """Walks the specified directory looking for pkg(7) manifests. 670 """ 671 for mfile in os.listdir(mdir): 672 if (mfile.endswith(".mog") and 673 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)): 674 try: 675 self.addmanifest(mdir, mfile, arch, modechecks, exceptions) 676 except IOError as exc: 677 sys.stderr.write("warning: %s\n" % str(exc)) 678 679 def resolvehardlinks(self): 680 """Populates mode, group, and owner for resolved (ie link target 681 is present in the manifest tree) hard links. 682 """ 683 for info in list(self.values()): 684 if info.name() == "hardlink": 685 tgt = info.hardkey 686 if tgt in self: 687 tgtinfo = self[tgt] 688 info.owner = tgtinfo.owner 689 info.group = tgtinfo.group 690 info.mode = tgtinfo.mode 691 692class ExceptionList(set): 693 """Keep track of an exception list as a set of paths to be excluded 694 from any other lists we build. 695 """ 696 697 def __init__(self, files, arch): 698 set.__init__(self) 699 for fname in files: 700 try: 701 self.readexceptionfile(fname, arch) 702 except IOError as exc: 703 sys.stderr.write("warning: cannot read exception file: %s\n" % 704 str(exc)) 705 706 def readexceptionfile(self, efile, arch): 707 """Build a list of all pathnames from the specified file that 708 either apply to all architectures (ie which have no trailing 709 architecture tokens), or to the specified architecture (ie 710 which have the value of the arch arg as a trailing 711 architecture token.) 712 """ 713 714 excfile = open(efile) 715 716 for exc in excfile: 717 exc = exc.split() 718 if len(exc) and exc[0][0] != "#": 719 if arch in (exc[1:] or arch): 720 self.add(os.path.normpath(exc[0])) 721 722 excfile.close() 723 724 725USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2] 726 727where input_1 and input_2 may specify proto lists, proto areas, 728or manifest directories. For proto lists, use one or more 729 730 -l file 731 732arguments. For proto areas, use one or more 733 734 -p dir 735 736arguments. For manifest directories, use one or more 737 738 -m dir 739 740arguments. 741 742If -L or -M is specified, then only one input source is allowed, and 743it should be one or more manifest directories. These two options are 744mutually exclusive. 745 746The -L option is used to generate a proto list to stdout. 747 748The -M option is used to check for safe file and directory modes. 749By default, this causes all mode checks to be performed. Individual 750mode checks may be turned off using "-X check," where "check" comes 751from the following set of checks: 752 753 m check for group or other write permissions 754 w check for user write permissions on files and directories 755 not owned by root 756 s check for group/other read permission on executable files 757 that have setuid/setgid bit(s) 758 o check for files that could be safely owned by root 759""" % sys.argv[0] 760 761 762def usage(msg=None): 763 """Try to give the user useful information when they don't get the 764 command syntax right. 765 """ 766 if msg: 767 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) 768 sys.stderr.write(USAGE) 769 sys.exit(2) 770 771 772def main(argv): 773 """Compares two out of three possible data sources: a proto list, a 774 set of proto areas, and a set of manifests. 775 """ 776 try: 777 opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:') 778 except getopt.GetoptError as exc: 779 usage(str(exc)) 780 781 if args: 782 usage() 783 784 arch = None 785 exceptionlists = [] 786 listonly = False 787 manifestdirs = [] 788 manifesttree = ManifestTree("manifests") 789 protodirs = [] 790 prototree = ProtoTree("proto area") 791 protolists = [] 792 protolist = ProtoTree("proto list") 793 modechecks = set() 794 togglemodechecks = set() 795 trees = [] 796 comparing = set() 797 verbose = False 798 799 for opt, arg in opts: 800 if opt == "-a": 801 if arch: 802 usage("may only specify one architecture") 803 else: 804 arch = arg 805 elif opt == "-e": 806 exceptionlists.append(arg) 807 elif opt == "-L": 808 listonly = True 809 elif opt == "-l": 810 comparing.add("protolist") 811 protolists.append(os.path.normpath(arg)) 812 elif opt == "-M": 813 modechecks.update(DEFAULTMODECHECKS) 814 elif opt == "-m": 815 comparing.add("manifests") 816 manifestdirs.append(os.path.normpath(arg)) 817 elif opt == "-p": 818 comparing.add("proto area") 819 protodirs.append(os.path.normpath(arg)) 820 elif opt == "-v": 821 verbose = True 822 elif opt == "-X": 823 togglemodechecks.add(arg) 824 825 if listonly or len(modechecks) > 0: 826 if len(comparing) != 1 or "manifests" not in comparing: 827 usage("-L and -M require one or more -m args, and no -l or -p") 828 if listonly and len(modechecks) > 0: 829 usage("-L and -M are mutually exclusive") 830 elif len(comparing) != 2: 831 usage("must specify exactly two of -l, -m, and -p") 832 833 if len(togglemodechecks) > 0 and len(modechecks) == 0: 834 usage("-X requires -M") 835 836 for s in togglemodechecks: 837 if s not in ALLMODECHECKS: 838 usage("unknown mode check %s" % s) 839 modechecks.symmetric_difference_update((s)) 840 841 if len(modechecks) == 0: 842 modechecks = None 843 844 if not arch: 845 usage("must specify architecture") 846 847 exceptions = ExceptionList(exceptionlists, arch) 848 originalexceptions = exceptions.copy() 849 850 if len(manifestdirs) > 0: 851 for mdir in manifestdirs: 852 manifesttree.adddir(mdir, arch, modechecks, exceptions) 853 if listonly: 854 manifesttree.resolvehardlinks() 855 for info in list(manifesttree.values()): 856 print("%s" % info.protostr()) 857 sys.exit(0) 858 if modechecks is not None: 859 sys.exit(0) 860 trees.append(manifesttree) 861 862 if len(protodirs) > 0: 863 for pdir in protodirs: 864 prototree.adddir(pdir, exceptions) 865 trees.append(prototree) 866 867 if len(protolists) > 0: 868 for plist in protolists: 869 try: 870 protolist.addprotolist(plist, exceptions) 871 except IOError as exc: 872 sys.stderr.write("warning: %s\n" % str(exc)) 873 trees.append(protolist) 874 875 if verbose and exceptions: 876 print("Entries present in exception list but missing from proto area:") 877 for exc in sorted(exceptions): 878 print("\t%s" % exc) 879 print("") 880 881 usedexceptions = originalexceptions.difference(exceptions) 882 harmfulexceptions = usedexceptions.intersection(manifesttree) 883 if harmfulexceptions: 884 print("Entries present in exception list but also in manifests:") 885 for exc in sorted(harmfulexceptions): 886 print("\t%s" % exc) 887 del manifesttree[exc] 888 print("") 889 890 trees[0].compare(trees[1]) 891 892if __name__ == '__main__': 893 locale.setlocale(locale.LC_ALL, "") 894 gettext.install("pkg", "/usr/share/locale") 895 896 try: 897 main(sys.argv[1:]) 898 except KeyboardInterrupt: 899 sys.exit(1) 900 except IOError: 901 sys.exit(1) 902