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)