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