#!/usr/bin/python2.7 # # # # # # # # # # # # # # # # # # # # # # Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. # import getopt import gettext import locale import sys import traceback import pkg.actions import pkg.variant as variant import pkg.client.api_errors as apx import pkg.manifest as manifest import pkg.misc as misc from pkg.misc import PipeError from collections import defaultdict def usage(errmsg="", exitcode=2): """Emit a usage message and optionally prefix it with a more specific error message. Causes program to exit.""" if errmsg: print >> sys.stderr, "pkgdiff: %s" % errmsg print _("""\ Usage: pkgdiff [-i attribute]... [-o attribute] [-t action_name[,action_name]...]... [-v name=value]... (file1 | -) (file2 | -)""") sys.exit(exitcode) def error(text, exitcode=3): """Emit an error message prefixed by the command name """ print >> sys.stderr, "pkgdiff: %s" % text if exitcode != None: sys.exit(exitcode) def main_func(): gettext.install("pkg", "/usr/share/locale", codeset=locale.getpreferredencoding()) ignoreattrs = [] onlyattrs = [] onlytypes = [] varattrs = defaultdict(set) try: opts, pargs = getopt.getopt(sys.argv[1:], "i:o:t:v:?", ["help"]) for opt, arg in opts: if opt == "-i": ignoreattrs.append(arg) elif opt == "-o": onlyattrs.append(arg) elif opt == "-t": onlytypes.extend(arg.split(",")) elif opt == "-v": args = arg.split("=") if len(args) != 2: usage(_("variant option incorrect %s") % arg) if not args[0].startswith("variant."): args[0] = "variant." + args[0] varattrs[args[0]].add(args[1]) elif opt in ("--help", "-?"): usage(exitcode=0) except getopt.GetoptError, e: usage(_("illegal global option -- %s") % e.opt) if len(pargs) != 2: usage(_("two manifest arguments are required")) if (pargs[0] == "-" and pargs[1] == "-"): usage(_("only one manifest argument can be stdin")) if ignoreattrs and onlyattrs: usage(_("-i and -o options may not be used at the same time.")) for v in varattrs: if len(varattrs[v]) > 1: usage(_("For any variant, only one value may be specified.")) varattrs[v] = varattrs[v].pop() ignoreattrs = set(ignoreattrs) onlyattrs = set(onlyattrs) onlytypes = set(onlytypes) utypes = set( t for t in onlytypes if t == "generic" or t not in pkg.actions.types ) if utypes: usage(_("unknown action types: %s" % apx.list_to_lang(list(utypes)))) manifest1 = manifest.Manifest() manifest2 = manifest.Manifest() try: # This assumes that both pargs are not '-'. for p, m in zip(pargs, (manifest1, manifest2)): if p == "-": m.set_content(content=sys.stdin.read()) else: m.set_content(pathname=p) except (pkg.actions.ActionError, apx.InvalidPackageErrors), e: error(_("Action error in file %(p)s: %(e)s") % locals()) except (EnvironmentError, apx.ApiException), e: error(e) # # manifest filtering # # filter action type if onlytypes: for m in (manifest1, manifest2): # Must pass complete list of actions to set_content, not # a generator, to avoid clobbering manifest contents. m.set_content(content=list(m.gen_actions_by_types( onlytypes))) # filter variant v1 = manifest1.get_all_variants() v2 = manifest2.get_all_variants() for vname in varattrs: for path, v, m in zip(pargs, (v1, v2), (manifest1, manifest2)): if vname not in v: continue filt = varattrs[vname] if filt not in v[vname]: usage(_("Manifest %(path)s doesn't support variant %(vname)s=%(filt)s" % locals())) # remove the variant tag def rip(a): a.attrs.pop(vname, None) return a m.set_content([ rip(a) for a in m.gen_actions(excludes=[ variant.Variants({vname: filt}).allow_action]) ]) m[vname] = filt if varattrs: # need to rebuild these if we're filtering variants v1 = manifest1.get_all_variants() v2 = manifest2.get_all_variants() # we need to be a little clever about variants, since # we can have multiple actions w/ the same key attributes # in each manifest in that case. First, make sure any variants # of the same name have the same values defined. for k in set(v1.keys()) & set(v2.keys()): if v1[k] != v2[k]: error(_("Manifests support different variants " "%(v1)s %(v2)s") % {"v1": v1, "v2": v2}) # Now, get a list of all possible variant values, including None # across all variants and both manifests v_values = dict() for v in v1: v1[v].add(None) for a in v1[v]: v_values.setdefault(v, set()).add((v, a)) for v in v2: v2[v].add(None) for a in v2[v]: v_values.setdefault(v, set()).add((v, a)) diffs = [] for tup in product(*v_values.values()): # build excludes closure to examine only actions exactly # matching current variant values... this is needed to # avoid confusing manifest difference code w/ multiple # actions w/ same key attribute values or getting dups # in output def allow(a, publisher=None): for k, v in tup: if v is not None: if k not in a.attrs or a.attrs[k] != v: return False elif k in a.attrs: return False return True a, c, r = manifest2.difference(manifest1, [allow], [allow]) diffs += a diffs += c diffs += r # License action still causes spurious diffs... check again for now. real_diffs = [ (a,b) for a, b in diffs if a is None or b is None or a.different(b) ] if not real_diffs: return 0 # define some ordering functions so that output is easily readable # First, a human version of action comparison that works across # variants and action changes... def compare(a, b): if hasattr(a, "key_attr") and hasattr(b, "key_attr") and \ a.key_attr == b.key_attr: res = cmp(a.attrs[a.key_attr], b.attrs[b.key_attr]) if res: return res # sort by variant res = cmp(sorted(list(a.get_variant_template())), sorted(list(b.get_variant_template()))) if res: return res else: res = cmp(a.ordinality, b.ordinality) if res: return res return cmp(str(a), str(b)) # and something to pull the relevant action out of the old value, new # value tuples def tuple_key(a): if not a[0]: return a[1] return a[0] # sort and.... diffs = sorted(diffs, key=tuple_key, cmp=compare) # handle list attributes def attrval(attrs, k, elide_iter=tuple()): def q(s): if " " in s or s == "": return '"%s"' % s else: return s v = attrs[k] if isinstance(v, list) or isinstance(v, set): out = " ".join(["%s=%s" % (k, q(lmt)) for lmt in sorted(v) if lmt not in elide_iter]) elif " " in v or v == "": out = k + "=\"" + v + "\"" else: out = k + "=" + v return out # figure out when to print diffs def conditional_print(s, a): if onlyattrs: if not set(a.attrs.keys()) & onlyattrs: return False elif ignoreattrs: if not set(a.attrs.keys()) - ignoreattrs: return False print "%s %s" % (s, a) return True different = False for old, new in diffs: if not new: different |= conditional_print("-", old) elif not old: different |= conditional_print("+", new) else: s = [] if not onlyattrs: if hasattr(old, "hash") and "hash" not in ignoreattrs: if old.hash != new.hash: s.append(" - %s" % new.hash) s.append(" + %s" % old.hash) attrdiffs = set(new.differences(old)) - ignoreattrs attrsames = sorted(list(set(old.attrs.keys() + new.attrs.keys()) - set(new.differences(old)))) else: if hasattr(old, "hash") and "hash" in onlyattrs: if old.hash != new.hash: s.append(" - %s" % new.hash) s.append(" + %s" % old.hash) attrdiffs = set(new.differences(old)) & onlyattrs attrsames = sorted(list(set(old.attrs.keys() + new.attrs.keys()) - set(new.differences(old)))) for a in sorted(attrdiffs): if a in old.attrs and a in new.attrs and \ isinstance(old.attrs[a], list) and \ isinstance(new.attrs[a], list): elide_set = set(old.attrs[a]) & set(new.attrs[a]) else: elide_set = set() if a in old.attrs: diff_str = attrval(old.attrs, a, elide_iter=elide_set) if diff_str: s.append(" - %s" % diff_str) if a in new.attrs: diff_str = attrval(new.attrs, a, elide_iter=elide_set) if diff_str: s.append(" + %s" % diff_str) # print out part of action that is the same if s: different = True print "%s %s %s" % (old.name, attrval(old.attrs, old.key_attr), " ".join(("%s" % attrval(old.attrs,v) for v in attrsames if v != old.key_attr))) for l in s: print l return int(different) def product(*args, **kwds): # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 # from python 2.7 itertools pools = map(tuple, args) * kwds.get('repeat', 1) result = [[]] for pool in pools: result = [x+[y] for x in result for y in pool] for prod in result: yield tuple(prod) if __name__ == "__main__": try: exit_code = main_func() except (PipeError, KeyboardInterrupt): exit_code = 1 except SystemExit, __e: exit_code = __e except Exception, __e: traceback.print_exc() error(misc.get_traceback_message(), exitcode=None) exit_code = 99 sys.exit(exit_code)