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