xref: /titanic_41/usr/src/tools/scripts/validate_pkg.py (revision dbba828ecad090dcb1319c4093c9ab74f63d6a95)
1 #!/usr/bin/python2.6
2 #
3 # CDDL HEADER START
4 #
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
8 #
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
13 #
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
19 #
20 # CDDL HEADER END
21 #
22 
23 #
24 # Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
25 # Use is subject to license terms.
26 #
27 
28 #
29 # Compare the content generated by a build to a set of manifests
30 # describing how that content is to be delivered.
31 #
32 
33 
34 import getopt
35 import os
36 import stat
37 import sys
38 
39 from pkg import actions
40 from pkg import manifest
41 
42 
43 #
44 # Dictionary used to map action names to output format.  Each entry is
45 # indexed by action name, and consists of a list of tuples that map
46 # FileInfo class members to output labels.
47 #
48 OUTPUTMAP = {
49     "dir": [
50         ("group", "group="),
51         ("mode", "mode="),
52         ("owner", "owner="),
53         ("path", "path=")
54     ],
55     "file": [
56         ("hash", ""),
57         ("group", "group="),
58         ("mode", "mode="),
59         ("owner", "owner="),
60         ("path", "path=")
61     ],
62     "link": [
63         ("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
73 ALLMODECHECKS = frozenset(("m", "w", "s", "o"))
74 DEFAULTMODECHECKS = frozenset(("m", "w", "o"))
75 
76 class 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 
284 class 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 
327 class 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 
343 class 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 
394 class 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 
438 class 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 
447 class 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 
551 class 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 
565 class 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 = None
639 
640             #
641             # The name of this method changed in pkg(5) build 150, we need to
642             # work with both sets.
643             #
644             if hasattr(action, 'get_variants'):
645                 var = action.get_variants()
646             else:
647                 var = action.get_variant_template()
648             if "variant.arch" in var and arch not in var["variant.arch"]:
649                 return
650 
651             self[path] = ActionInfo(action)
652             if modechecks is not None and path not in exceptions:
653                 modewarnings.update(self[path].checkmodes(modechecks))
654 
655         if len(modewarnings) > 0:
656             print "warning: unsafe permissions in %s" % mfile
657             for w in sorted(modewarnings):
658                 print w
659             print ""
660 
661     def adddir(self, mdir, arch, modechecks, exceptions):
662         """Walks the specified directory looking for pkg(5) manifests.
663         """
664         for mfile in os.listdir(mdir):
665             if (mfile.endswith(".mog") and
666                 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)):
667                 try:
668                     self.addmanifest(mdir, mfile, arch, modechecks, exceptions)
669                 except IOError, exc:
670                     sys.stderr.write("warning: %s\n" % str(exc))
671 
672     def resolvehardlinks(self):
673         """Populates mode, group, and owner for resolved (ie link target
674         is present in the manifest tree) hard links.
675         """
676         for info in self.values():
677             if info.name() == "hardlink":
678                 tgt = info.hardkey
679                 if tgt in self:
680                     tgtinfo = self[tgt]
681                     info.owner = tgtinfo.owner
682                     info.group = tgtinfo.group
683                     info.mode = tgtinfo.mode
684 
685 class ExceptionList(set):
686     """Keep track of an exception list as a set of paths to be excluded
687     from any other lists we build.
688     """
689 
690     def __init__(self, files, arch):
691         set.__init__(self)
692         for fname in files:
693             try:
694                 self.readexceptionfile(fname, arch)
695             except IOError, exc:
696                 sys.stderr.write("warning: cannot read exception file: %s\n" %
697                     str(exc))
698 
699     def readexceptionfile(self, efile, arch):
700         """Build a list of all pathnames from the specified file that
701         either apply to all architectures (ie which have no trailing
702         architecture tokens), or to the specified architecture (ie
703         which have the value of the arch arg as a trailing
704         architecture token.)
705         """
706 
707         excfile = open(efile)
708 
709         for exc in excfile:
710             exc = exc.split()
711             if len(exc) and exc[0][0] != "#":
712                 if arch in (exc[1:] or arch):
713                     self.add(os.path.normpath(exc[0]))
714 
715         excfile.close()
716 
717 
718 USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2]
719 
720 where input_1 and input_2 may specify proto lists, proto areas,
721 or manifest directories.  For proto lists, use one or more
722 
723     -l file
724 
725 arguments.  For proto areas, use one or more
726 
727     -p dir
728 
729 arguments.  For manifest directories, use one or more
730 
731     -m dir
732 
733 arguments.
734 
735 If -L or -M is specified, then only one input source is allowed, and
736 it should be one or more manifest directories.  These two options are
737 mutually exclusive.
738 
739 The -L option is used to generate a proto list to stdout.
740 
741 The -M option is used to check for safe file and directory modes.
742 By default, this causes all mode checks to be performed.  Individual
743 mode checks may be turned off using "-X check," where "check" comes
744 from the following set of checks:
745 
746     m   check for group or other write permissions
747     w   check for user write permissions on files and directories
748         not owned by root
749     s   check for group/other read permission on executable files
750         that have setuid/setgid bit(s)
751     o   check for files that could be safely owned by root
752 """ % sys.argv[0]
753 
754 
755 def usage(msg=None):
756     """Try to give the user useful information when they don't get the
757     command syntax right.
758     """
759     if msg:
760         sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
761     sys.stderr.write(USAGE)
762     sys.exit(2)
763 
764 
765 def main(argv):
766     """Compares two out of three possible data sources: a proto list, a
767     set of proto areas, and a set of manifests.
768     """
769     try:
770         opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:')
771     except getopt.GetoptError, exc:
772         usage(str(exc))
773 
774     if args:
775         usage()
776 
777     arch = None
778     exceptionlists = []
779     listonly = False
780     manifestdirs = []
781     manifesttree = ManifestTree("manifests")
782     protodirs = []
783     prototree = ProtoTree("proto area")
784     protolists = []
785     protolist = ProtoTree("proto list")
786     modechecks = set()
787     togglemodechecks = set()
788     trees = []
789     comparing = set()
790     verbose = False
791 
792     for opt, arg in opts:
793         if opt == "-a":
794             if arch:
795                 usage("may only specify one architecture")
796             else:
797                 arch = arg
798         elif opt == "-e":
799             exceptionlists.append(arg)
800         elif opt == "-L":
801             listonly = True
802         elif opt == "-l":
803             comparing.add("protolist")
804             protolists.append(os.path.normpath(arg))
805         elif opt == "-M":
806             modechecks.update(DEFAULTMODECHECKS)
807         elif opt == "-m":
808             comparing.add("manifests")
809             manifestdirs.append(os.path.normpath(arg))
810         elif opt == "-p":
811             comparing.add("proto area")
812             protodirs.append(os.path.normpath(arg))
813         elif opt == "-v":
814             verbose = True
815         elif opt == "-X":
816             togglemodechecks.add(arg)
817 
818     if listonly or len(modechecks) > 0:
819         if len(comparing) != 1 or "manifests" not in comparing:
820             usage("-L and -M require one or more -m args, and no -l or -p")
821         if listonly and len(modechecks) > 0:
822             usage("-L and -M are mutually exclusive")
823     elif len(comparing) != 2:
824         usage("must specify exactly two of -l, -m, and -p")
825 
826     if len(togglemodechecks) > 0 and len(modechecks) == 0:
827         usage("-X requires -M")
828 
829     for s in togglemodechecks:
830         if s not in ALLMODECHECKS:
831             usage("unknown mode check %s" % s)
832         modechecks.symmetric_difference_update((s))
833 
834     if len(modechecks) == 0:
835         modechecks = None
836 
837     if not arch:
838         usage("must specify architecture")
839 
840     exceptions = ExceptionList(exceptionlists, arch)
841     originalexceptions = exceptions.copy()
842 
843     if len(manifestdirs) > 0:
844         for mdir in manifestdirs:
845             manifesttree.adddir(mdir, arch, modechecks, exceptions)
846         if listonly:
847             manifesttree.resolvehardlinks()
848             for info in manifesttree.values():
849                 print "%s" % info.protostr()
850             sys.exit(0)
851         if modechecks is not None:
852             sys.exit(0)
853         trees.append(manifesttree)
854 
855     if len(protodirs) > 0:
856         for pdir in protodirs:
857             prototree.adddir(pdir, exceptions)
858         trees.append(prototree)
859 
860     if len(protolists) > 0:
861         for plist in protolists:
862             try:
863                 protolist.addprotolist(plist, exceptions)
864             except IOError, exc:
865                 sys.stderr.write("warning: %s\n" % str(exc))
866         trees.append(protolist)
867 
868     if verbose and exceptions:
869         print "Entries present in exception list but missing from proto area:"
870         for exc in sorted(exceptions):
871             print "\t%s" % exc
872         print ""
873 
874     usedexceptions = originalexceptions.difference(exceptions)
875     harmfulexceptions = usedexceptions.intersection(manifesttree)
876     if harmfulexceptions:
877         print "Entries present in exception list but also in manifests:"
878         for exc in sorted(harmfulexceptions):
879             print "\t%s" % exc
880             del manifesttree[exc]
881         print ""
882 
883     trees[0].compare(trees[1])
884 
885 if __name__ == '__main__':
886     try:
887         main(sys.argv[1:])
888     except KeyboardInterrupt:
889         sys.exit(1)
890     except IOError:
891         sys.exit(1)
892