15654935 pkg facet/variant should also display those implicitly set s12b25
authorShawn Walker <shawn.walker@oracle.com>
Wed, 19 Jun 2013 15:54:08 -0700
changeset 2910 2bb9f6ffdff9
parent 2909 bcafb737718d
child 2912 2f30aba343cd
15654935 pkg facet/variant should also display those implicitly set 15713232 pkg change-facet/change-variant could be faster 16425698 pkg variant subcommand should not require "variant." prefix 16689413 facet/variant subcommands should support wildcards 16689420 pkg facet/variant should offer parsable output format 16689425 pkg facet/variant should have a full test suite 16694970 manifest facets/variants properties can raise misleading exception 16803226 no updates messaging omits linked image name 16808780 pkg variant subcommand could show available variants
doc/client_api_versions.txt
exception_lists/keywords
src/client.py
src/man/pkg.1
src/modules/api_common.py
src/modules/client/api.py
src/modules/client/imageplan.py
src/modules/client/plandesc.py
src/modules/gui/misc_non_gui.py
src/modules/gui/preferences.py
src/modules/lint/engine.py
src/modules/manifest.py
src/modules/misc.py
src/modules/server/api.py
src/modules/server/depot.py
src/pkgdep.py
src/sysrepo.py
src/tests/cli/t_change_facet.py
src/tests/cli/t_change_variant.py
src/tests/cli/t_pkg_install.py
src/tests/cli/t_pkg_varcet.py
src/tests/cli/t_pkgdep_resolve.py
src/tests/pkg5unittest.py
--- a/doc/client_api_versions.txt	Wed Jun 12 15:44:50 2013 -0700
+++ b/doc/client_api_versions.txt	Wed Jun 19 15:54:08 2013 -0700
@@ -1,5 +1,15 @@
+Version 75:
+Compatible with clients using versions 72-74.
+
+    pkg.client.api.ImageInterface has changed as follows:
+
+	* New generator functions 'gen_variants()' and 'gen_facets()' to
+	  retrieve the current list of facets or variants currently
+	  (implicitly or explicitly) set in the image.  See 'pydoc
+	  pkg.client.api' for details.
+
 Version 74:
-Compatible with clients using version 73, 72.
+Compatible with clients using versions 72-74.
      The PlanDescription now has interfaces to
      determine whether or not release notes were generated
      for this operation, whether or not they must be displayed,
--- a/exception_lists/keywords	Wed Jun 12 15:44:50 2013 -0700
+++ b/exception_lists/keywords	Wed Jun 19 15:54:08 2013 -0700
@@ -20,9 +20,9 @@
 #
 
 #
-# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
+src/modules/client/api.py
 src/modules/client/image.py
 src/client.py
--- a/src/client.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/client.py	Wed Jun 19 15:54:08 2013 -0700
@@ -62,7 +62,6 @@
         import textwrap
         import time
         import traceback
-        import tempfile
 
         import pkg
         import pkg.actions as actions
@@ -92,7 +91,7 @@
         import sys
         sys.exit(1)
 
-CLIENT_API_VERSION = 74
+CLIENT_API_VERSION = 75
 PKG_CLIENT_NAME = "pkg"
 
 JUST_UNKNOWN = 0
@@ -272,8 +271,8 @@
             "            [--deny-new-be | --require-new-be] [--be-name name]\n"
             "            <mediator> ...")
 
-        adv_usage["variant"] = _("[-H] [<variant_spec>]")
-        adv_usage["facet"] = ("[-H] [<facet_spec>]")
+        adv_usage["variant"] = _("[-Haiv] [-F format] [<variant_pattern> ...]")
+        adv_usage["facet"] = ("[-Hai] [-F format] [<facet_pattern> ...]")
         adv_usage["avoid"] = _("[pkg_fmri_pattern] ...")
         adv_usage["unavoid"] = _("[pkg_fmri_pattern] ...")
         adv_usage["freeze"] = _("[-n] [-c reason] [pkg_fmri_pattern] ...")
@@ -346,10 +345,10 @@
                                         raise ValueError(
                                             "Unable to find usage str for %s" %
                                             cmd)
-                                usage = cmd_dic[cmd]
-                                if usage is not "":
+                                use_txt = cmd_dic[cmd]
+                                if use_txt is not "":
                                         logger.error(
-                                            "        pkg %(cmd)s %(usage)s" %
+                                            "        pkg %(cmd)s %(use_txt)s" %
                                             locals())
                                 else:
                                         logger.error("        pkg %s" % cmd)
@@ -996,7 +995,9 @@
                 elif src != dest:
                         c.append((src, dest))
                 else:
-                        a.append((src, dest))
+                        # Changing or repairing package content (e.g. fix,
+                        # change-facet, etc.)
+                        a.append((dest, dest))
 
         def bool_str(val):
                 if val:
@@ -1014,8 +1015,15 @@
                 cond_show(_("Packages to remove:"), "%d", len(r))
                 cond_show(_("Packages to install:"), "%d", len(i))
                 cond_show(_("Packages to update:"), "%d", len(c))
+                if varcets or mediators:
+                        cond_show(_("Packages to change:"), "%d", len(a))
+                else:
+                        cond_show(_("Packages to fix:"), "%d", len(a))
                 cond_show(_("Mediators to change:"), "%d", len(mediators))
                 cond_show(_("Variants/Facets to change:"), "%d", len(varcets))
+                if not plan.new_be:
+                        cond_show(_("Services to change:"), "%d",
+                            len(plan.services))
 
                 if verbose:
                         # Only show space information in verbose mode.
@@ -1027,11 +1035,6 @@
                                     _("Estimated space to be consumed:"),
                                     misc.bytes_to_str(plan.bytes_added)))
 
-                if varcets or mediators:
-                        cond_show(_("Packages to change:"), "%d", len(a))
-                else:
-                        cond_show(_("Packages to fix:"), "%d", len(a))
-
                 # only display BE information if we're operating on the
                 # liveroot environment (since otherwise we'll never be
                 # manipulating BEs).
@@ -1048,10 +1051,6 @@
                         status.append((_("Create backup boot environment:"),
                             bool_str(plan.backup_be)))
 
-                if not plan.new_be:
-                        cond_show(_("Services to change:"), "%d",
-                            len(plan.services))
-
         if "boot-archive" in disp:
                 status.append((_("Rebuild boot archive:"),
                     bool_str(plan.update_boot_archive)))
@@ -1087,7 +1086,7 @@
 
         if "fmris" in disp:
                 changed = collections.defaultdict(list)
-                for src, dest in itertools.chain(r, i, c):
+                for src, dest in itertools.chain(r, i, c, a):
                         if src and dest:
                                 if src.publisher != dest.publisher:
                                         pparent = "%s -> %s" % (src.publisher,
@@ -1095,8 +1094,9 @@
                                 else:
                                         pparent = dest.publisher
                                 pname = dest.pkg_stem
-                                pver = "%s -> %s" % (src.fmri.version,
-                                    dest.fmri.version)
+                                pver = str(src.fmri.version)
+                                if src != dest:
+                                        pver += " -> %s" % dest.fmri.version
                         elif dest:
                                 pparent = dest.publisher
                                 pname = dest.pkg_stem
@@ -1123,11 +1123,6 @@
                                 logger.info("    %s" % pver)
                                 last_parent = pparent
 
-                if len(a):
-                        logger.info(_("Affected fmris:"))
-                        for src, dest in a:
-                                logger.info("  %s", src)
-
         if "services" in disp and not plan.new_be:
                 last_action = None
                 for action, smf_fmri in plan.services:
@@ -1324,7 +1319,7 @@
                 else:
                         s = _("No updates necessary for this image.")
                 if api_inst.ischild():
-                        s + " (%s)" % api_inst.get_linked_name()
+                        s += " (%s)" % api_inst.get_linked_name()
                 msg(s)
                 return
 
@@ -1475,13 +1470,6 @@
 
 def __api_alloc(imgdir, exact_match, pkg_image_used):
 
-        def qv(val):
-                # Escape shell metacharacters; '\' must be escaped first to
-                # prevent escaping escapes.
-                for c in "\\ \t\n'`;&()|^<>?*":
-                        val = val.replace(c, "\\" + c)
-                return val
-
         progresstracker = get_tracker()
         try:
                 return api.ImageInterface(imgdir, CLIENT_API_VERSION,
@@ -3938,8 +3926,8 @@
                             duplicate=True)
                         dest_repo = dest_pub.repository
                         if dest_repo.origins and \
-                                not dest_repo.has_origin(repo_uri):
-                                        add_origins = [repo_uri]
+                            not dest_repo.has_origin(repo_uri):
+                                add_origins = [repo_uri]
 
                         if not src_repo and not add_origins:
                                 # The repository doesn't have to provide origin
@@ -4746,72 +4734,141 @@
 
         return EXIT_OK
 
-def variant_list(api_inst, args):
-        """pkg variant [-H] [<variant_spec>]"""
-
-        omit_headers = False
-
-        opts, pargs = getopt.getopt(args, "H")
-
-        for opt, arg in opts:
-                if opt == "-H":
-                        omit_headers = True
-
-        # XXX image variants should be accessible through pkg.client.api
-        variants = img.get_variants()
-
-        for p in pargs:
-                if p not in variants:
-                        error(_("no such variant: %s") % p, cmd="variant")
-                        return EXIT_OOPS
-
-        if not pargs:
-                pargs = variants.keys()
-
-        width = max(max([len(p) for p in pargs]), 8)
-        fmt = "%%-%ss %%s" % width
-        if not omit_headers:
-                msg(fmt % ("VARIANT", "VALUE"))
-
-        for p in pargs:
-                msg(fmt % (p, variants[p]))
-
+def list_variant(op, api_inst, pargs, omit_headers, output_format,
+    list_all_items, list_installed, verbose): 
+        """pkg variant [-Haiv] [-F format] [<variant_pattern> ...]"""
+
+        subcommand = "variant"
+        if output_format is None:
+                output_format = "default"
+
+        # To work around Python 2.x's scoping limits, a list is used.
+        found = [False]
+        req_variants = set(pargs)
+
+        def gen_current():
+                for (name, val, pvals) in api_inst.gen_variants(variant_list,
+                    patterns=req_variants):
+                        found[0] = True
+                        yield {
+                            "variant": name,
+                            "value": val
+                        }
+
+        def gen_possible():
+                for (name, val, pvals) in api_inst.gen_variants(variant_list,
+                    patterns=req_variants):
+                        found[0] = True
+                        for pval in pvals:
+                                yield {
+                                    "variant": name,
+                                    "value": pval
+                                }
+
+        if verbose:
+                gen_listing = gen_possible
+        else:
+                gen_listing = gen_current
+
+        if list_all_items:
+                if verbose:
+                        variant_list = api_inst.VARIANT_ALL_POSSIBLE
+                else:
+                        variant_list = api_inst.VARIANT_ALL
+        elif list_installed:
+                if verbose:
+                        variant_list = api_inst.VARIANT_INSTALLED_POSSIBLE
+                else:
+                        variant_list = api_inst.VARIANT_INSTALLED
+        else:
+                if verbose:
+                        variant_list = api_inst.VARIANT_IMAGE_POSSIBLE
+                else:
+                        variant_list = api_inst.VARIANT_IMAGE
+
+        #    VARIANT VALUE
+        #    <variant> <value>
+        #    <variant_2> <value_2>
+        #    ...
+        field_data = {
+            "variant" : [("default", "json", "tsv"), _("VARIANT"), ""],
+            "value" : [("default", "json", "tsv"), _("VALUE"), ""],
+        }
+        desired_field_order = (_("VARIANT"), _("VALUE"))
+
+        # Default output formatting.
+        def_fmt = "%-70s %s"
+
+        # print without trailing newline.
+        sys.stdout.write(misc.get_listing(desired_field_order,
+            field_data, gen_listing(), output_format, def_fmt,
+            omit_headers))
+
+        if not found[0] and req_variants:
+                if output_format == "default":
+                        # Don't pollute other output formats.
+                        error(_("no matching variants found"),
+                            cmd=subcommand)
+                return EXIT_OOPS
+
+        # Successful if no variants exist or if at least one matched.
         return EXIT_OK
 
-def facet_list(api_inst, args):
-        """pkg facet [-H] [<facet_spec>]"""
-
-        omit_headers = False
-
-        opts, pargs = getopt.getopt(args, "H")
-
-        for opt, arg in opts:
-                if opt == "-H":
-                        omit_headers = True
-
-        # XXX image facets should be accessible through pkg.client.api
-        facets = img.get_facets()
-
-        for i, p in enumerate(pargs[:]):
-                if not p.startswith("facet."):
-                        pargs[i] = "facet." + p
-
-        if not pargs:
-                pargs = facets.keys()
-
-        if pargs:
-                width = max(max([len(p) for p in pargs]), 8)
-        else:
-                width = 8
-
-        fmt = "%%-%ss %%s" % width
-
-        if not omit_headers:
-                msg(fmt % ("FACETS", "VALUE"))
-
-        for p in pargs:
-                msg(fmt % (p, facets[p]))
-
+def list_facet(op, api_inst, pargs, omit_headers, output_format, list_all_items,
+    list_installed):
+        """pkg facet [-Hai] [-F format] [<facet_pattern> ...]"""
+
+        subcommand = "facet"
+        if output_format is None:
+                output_format = "default"
+
+        # To work around Python 2.x's scoping limits, a list is used.
+        found = [False]
+        req_facets = set(pargs)
+
+        facet_list = api_inst.FACET_IMAGE
+        if list_all_items:
+                facet_list = api_inst.FACET_ALL
+        elif list_installed:
+                facet_list = api_inst.FACET_INSTALLED
+
+        def gen_listing():
+                for (name, val) in api_inst.gen_facets(facet_list,
+                    patterns=req_facets):
+                        found[0] = True
+
+                        # Values here are intentionally not _().
+                        yield {
+                            "facet": name,
+                            "value": val and "True" or "False"
+                        }
+
+        #    FACET VALUE
+        #    <facet> <value>
+        #    <facet_2> <value_2>
+        #    ...
+        field_data = {
+            "facet" : [("default", "json", "tsv"), _("FACET"), ""],
+            "value" : [("default", "json", "tsv"), _("VALUE"), ""],
+        }
+        desired_field_order = (_("FACET"), _("VALUE"))
+
+        # Default output formatting.
+        def_fmt = "%-70s %s"
+
+        # print without trailing newline.
+        sys.stdout.write(misc.get_listing(desired_field_order,
+            field_data, gen_listing(), output_format, def_fmt,
+            omit_headers))
+
+        if not found[0] and req_facets:
+                if output_format == "default":
+                        # Don't pollute other output formats.
+                        error(_("no matching facets found"),
+                            cmd=subcommand)
+                return EXIT_OOPS
+
+        # Successful if no facets exist or if at least one matched.
         return EXIT_OK
 
 def list_linked(op, api_inst, pargs,
@@ -5765,6 +5822,7 @@
     "attach_parent" :     ("p",  ""),
 
     "list_available" :    ("a",  ""),
+    "list_all_items" :    ("a",  ""),
     "output_format" :     ("F",  "output-format"),
 
     "tagged" :            ("",  "tagged"),
@@ -5784,6 +5842,8 @@
 
     "ctlfd" :                 ("",  "ctlfd"),
     "progfd" :                ("",  "progfd"),
+
+    "list_installed" :        ("i",  ""),
 }
 
 #
@@ -5807,7 +5867,7 @@
     "change-variant"        : [change_variant],
     "contents"              : [list_contents],
     "detach-linked"         : [detach_linked, 0],
-    "facet"                 : [facet_list],
+    "facet"                 : [list_facet],
     "fix"                   : [fix_image],
     "freeze"                : [freeze],
     "help"                  : [None],
@@ -5840,12 +5900,11 @@
     "uninstall"             : [uninstall],
     "unset-authority"       : [publisher_unset],
     "unset-property"        : [property_unset],
-    "update-format"         : [update_format],
     "unset-mediator"        : [unset_mediator],
     "unset-publisher"       : [publisher_unset],
     "update"                : [update],
     "update-format"         : [update_format],
-    "variant"               : [variant_list],
+    "variant"               : [list_variant],
     "verify"                : [verify_image],
     "version"               : [None],
 }
@@ -5870,6 +5929,31 @@
     ("progfd",               None),
 ]
 
+def opts_cb_varcet(api_inst, opts, opts_new):
+        if opts_new["list_all_items"] and opts_new["list_installed"]:
+                raise api_errors.InvalidOptionError(
+                    api_errors.InvalidOptionError.INCOMPAT,
+                    ["list_all_items", "list_installed"])
+
+opts_list_varcet = \
+    options.opts_table_no_headers + \
+    [
+    opts_cb_varcet,
+    ("list_all_items",          False),
+    ("list_installed",          False),
+    ("output_format",           None)
+]
+
+opts_list_facet = \
+    [opts_cb_varcet] + \
+    opts_list_varcet
+
+opts_list_variant = \
+    opts_list_varcet + \
+    [
+    ("verbose",      False)
+]
+
 opts_list_mediator = \
     options.opts_table_no_headers + \
     [
@@ -5887,9 +5971,11 @@
 ]
 
 cmd_opts = {
-    "mediator"              : opts_list_mediator,
-    "unset-mediator"        : opts_unset_mediator,
-    "remote"                : opts_remote,
+    "facet"             : opts_list_facet,
+    "mediator"          : opts_list_mediator,
+    "unset-mediator"    : opts_unset_mediator,
+    "remote"            : opts_remote,
+    "variant"           : opts_list_variant,
 }
 
 
@@ -6067,7 +6153,6 @@
                 # when there is a short and a long version for the same option
                 # we print both to avoid confusion.
                 def get_cli_opt(option):
-                        out = ""
                         try:
                                 s, l = opts_mapping[option]
                                 if l and not s:
@@ -6075,7 +6160,7 @@
                                 elif s and not l:
                                         return "-%s" % s
                                 else:
-                                        return("-%s/--%s" % (s,l))
+                                        return "-%s/--%s" % (s, l)
                         except KeyError:
                                 # ignore if we can't find a match
                                 # (happens for repeated arguments)
--- a/src/man/pkg.1	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/man/pkg.1	Wed Jun 19 15:54:08 2013 -0700
@@ -1,6 +1,6 @@
 '\" te
 .\" Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
-.TH pkg 1 "26 Feb 2013" "SunOS 5.12" "User Commands"
+.TH pkg 1 "19 Apr 2013" "SunOS 5.12" "User Commands"
 .SH NAME
 pkg \- Image Packaging System retrieval client
 .SH SYNOPSIS
@@ -115,7 +115,7 @@
 
 .LP
 .nf
-/usr/bin/pkg variant [-H] [variant.\fIvariant_name\fR ...]
+/usr/bin/pkg variant [-Haiv] [-F \fIformat\fR] [\fIvariant_pattern\fR ...]
 .fi
 
 .LP
@@ -130,7 +130,7 @@
 
 .LP
 .nf
-/usr/bin/pkg facet [-H] [\fIfacet_name\fR ...]
+/usr/bin/pkg facet [-Hai] [-F \fIformat\fR] [\fIfacet_pattern\fR ...]
 .fi
 
 .LP
@@ -1576,7 +1576,7 @@
 .ne 2
 .mk
 .na
-\fB\fBpkg variant\fR [\fB-H\fR] [\fBvariant.\fR\fIvariant_name\fR ...]\fR
+\fB\fBpkg variant\fR [\fB-Haiv\fR] [\fB-F\fR \fIformat\fR] [\fIvariant_pattern\fR ...]\fR
 .ad
 .sp .6
 .RS 4n
@@ -1585,7 +1585,7 @@
 .ne 2
 .mk
 .na
-\fB\fBvariant.\fR\fIvariant_name\fR\fR
+\fB\fIvariant_pattern\fR\fR
 .ad
 .sp .6
 .RS 4n
@@ -1596,6 +1596,17 @@
 .ne 2
 .mk
 .na
+\fB\fB-F\fR\fR
+.ad
+.sp .6
+.RS 4n
+Specify an alternative output format. Currently, only \fBtsv\fR (Tab Separated Values) is valid.
+.RE
+
+.sp
+.ne 2
+.mk
+.na
 \fB\fB-H\fR\fR
 .ad
 .sp .6
@@ -1603,6 +1614,39 @@
 Omit the headers from the listing.
 .RE
 
+.sp
+.ne 2
+.mk
+.na
+\fB\fB-a\fR\fR
+.ad
+.sp .6
+.RS 4n
+Display all variants explicitly set in the image and all variants that are listed in installed packages. This option cannot be combined with \fB-i\fR.
+.RE
+
+.sp
+.ne 2
+.mk
+.na
+\fB\fB-i\fR\fR
+.ad
+.sp .6
+.RS 4n
+Display all variants that are listed in installed packages. This option cannot be combined with \fB-a\fR.
+.RE
+
+.sp
+.ne 2
+.mk
+.na
+\fB\fB-v\fR\fR
+.ad
+.sp .6
+.RS 4n
+Display the possible variant values that can be set for installed packages. This option can be combined with \fB-a\fR and \fB-i\fR.
+.RE
+
 .RE
 
 .sp
@@ -1624,7 +1668,7 @@
 .ne 2
 .mk
 .na
-\fB\fBpkg facet\fR [\fB-H\fR] [\fIfacet_name\fR ...]\fR
+\fB\fBpkg facet\fR [\fB-Hai\fR] [\fB-F\fR \fIformat\fR] [\fIfacet_pattern\fR ...]\fR
 .ad
 .sp .6
 .RS 4n
@@ -1633,7 +1677,7 @@
 .ne 2
 .mk
 .na
-\fB\fIfacet_name\fR\fR
+\fB\fIfacet_pattern\fR\fR
 .ad
 .sp .6
 .RS 4n
@@ -1644,6 +1688,17 @@
 .ne 2
 .mk
 .na
+\fB\fB-F\fR\fR
+.ad
+.sp .6
+.RS 4n
+Specify an alternative output format. Currently, only \fBtsv\fR (Tab Separated Values) is valid.
+.RE
+
+.sp
+.ne 2
+.mk
+.na
 \fB\fB-H\fR\fR
 .ad
 .sp .6
@@ -1651,6 +1706,28 @@
 Omit the headers from the listing.
 .RE
 
+.sp
+.ne 2
+.mk
+.na
+\fB\fB-a\fR\fR
+.ad
+.sp .6
+.RS 4n
+Display all facets explicitly set in the image and all facets that are listed in installed packages. This option cannot be combined with \fB-i\fR.
+.RE
+
+.sp
+.ne 2
+.mk
+.na
+\fB\fB-i\fR\fR
+.ad
+.sp .6
+.RS 4n
+Display all facets that are listed in installed packages. This option cannot be combined with \fB-a\fR.
+.RE
+
 .RE
 
 .sp
--- a/src/modules/api_common.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/api_common.py	Wed Jun 19 15:54:08 2013 -0700
@@ -133,8 +133,9 @@
         def __init__(self, pfmri, pkg_stem=None, summary=None,
             category_info_list=None, states=None, publisher=None,
             version=None, build_release=None, branch=None, packaging_date=None,
-            size=None, licenses=None, links=None, hardlinks=None, files=None,
-            dirs=None, dependencies=None, description=None, attrs=None):
+            size=None, csize=None, licenses=None, links=None, hardlinks=None,
+            files=None, dirs=None, dependencies=None, description=None,
+            attrs=None):
                 self.pkg_stem = pkg_stem
 
                 self.summary = summary
@@ -148,6 +149,7 @@
                 self.branch = branch
                 self.packaging_date = packaging_date
                 self.size = size
+                self.csize = csize
                 self.fmri = pfmri
                 self.licenses = licenses
                 self.links = links
--- a/src/modules/client/api.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/client/api.py	Wed Jun 19 15:54:08 2013 -0700
@@ -103,8 +103,8 @@
 # things like help(pkg.client.api.PlanDescription)
 from pkg.client.plandesc import PlanDescription # pylint: disable=W0611
 
-CURRENT_API_VERSION = 74
-COMPATIBLE_API_VERSIONS = frozenset([72, 73, CURRENT_API_VERSION])
+CURRENT_API_VERSION = 75
+COMPATIBLE_API_VERSIONS = frozenset([72, 73, 74, CURRENT_API_VERSION])
 CURRENT_P5I_VERSION = 1
 
 # Image type constants.
@@ -253,6 +253,10 @@
         needed.  Cancel may only be invoked while a cancelable method is
         running."""
 
+        FACET_ALL = 0
+        FACET_IMAGE = 1
+        FACET_INSTALLED = 2
+
         # Constants used to reference specific values that info can return.
         INFO_FOUND = 0
         INFO_MISSING = 1
@@ -268,6 +272,13 @@
         MATCH_FMRI = 1
         MATCH_GLOB = 2
 
+        VARIANT_ALL = 0
+        VARIANT_ALL_POSSIBLE = 1
+        VARIANT_IMAGE = 2
+        VARIANT_IMAGE_POSSIBLE = 3
+        VARIANT_INSTALLED = 4
+        VARIANT_INSTALLED_POSSIBLE = 5
+
         def __init__(self, img_path, version_id, progresstracker,
             cancel_state_callable, pkg_client_name, exact_match=True,
             cmdpath=None):
@@ -817,6 +828,173 @@
                 dependencies on this) """
                 return [a for a in self._img.get_avoid_dict().iteritems()]
 
+        def gen_facets(self, facet_list, patterns=misc.EmptyI):
+                """A generator function that produces tuples of the form:
+
+                    (
+                        name,    - (string) facet name (e.g. facet.doc)
+                        value    - (boolean) current facet value
+                    )
+
+                Results are always sorted by facet name.
+
+                'facet_list' is one of the following constant values indicating
+                which facets should be returned based on how they were set:
+
+                        FACET_ALL
+                                Return all facets set in the image and all
+                                facets listed in installed packages.
+
+                        FACET_IMAGE
+                                Return only the facets set in the image.
+
+                        FACET_INSTALLED
+                                Return only the facets listed in installed
+                                packages.
+
+                'patterns' is an optional list of facet wildcard strings to
+                filter results by."""
+
+                facets = self._img.cfg.facets
+                if facet_list != self.FACET_INSTALLED:
+                        # Include all facets set in image.
+                        fimg = set(facets.keys())
+                else:
+                        # Don't include any set only in image.
+                        fimg = set()
+
+                # Get all facets found in packages and determine state.
+                fpkg = set()
+                excludes = self._img.list_excludes()
+                if facet_list != self.FACET_IMAGE:
+                        for f in self._img.gen_installed_pkgs():
+                                # The manifest must be loaded without
+                                # pre-applying excludes so that gen_facets() can
+                                # choose how to filter the actions.
+                                mfst = self._img.get_manifest(f,
+                                    ignore_excludes=True)
+                                for facet in mfst.gen_facets(excludes=excludes):
+                                        # Use Facets object to determine
+                                        # effective facet state.
+                                        fpkg.add(facet)
+
+                # Generate the results.
+                for name in misc.yield_matching("facet.", sorted(fimg | fpkg),
+                    patterns):
+                        # The image's Facets dictionary will return the
+                        # effective value for any facets not explicitly set in
+                        # the image (wildcards or implicit).
+                        yield (name, facets[name])
+
+        def gen_variants(self, variant_list, patterns=misc.EmptyI):
+                """A generator function that produces tuples of the form:
+
+                    (
+                        name,    - (string) variant name (e.g. variant.arch)
+                        value    - (string) current variant value,
+                        possible - (list) list of possible variant values based
+                                   on installed packages; empty unless using
+                                   *_POSSIBLE variant_list.
+                    )
+
+                Results are always sorted by variant name.
+
+                'variant_list' is one of the following constant values indicating
+                which variants should be returned based on how they were set:
+
+                        VARIANT_ALL
+                                Return all variants set in the image and all
+                                variants listed in installed packages.
+
+                        VARIANT_ALL_POSSIBLE
+                                Return possible variant values (those found in
+                                any installed package) for all variants set in
+                                the image and all variants listed in installed
+                                packages.
+
+                        VARIANT_IMAGE
+                                Return only the variants set in the image.
+
+                        VARIANT_IMAGE_POSSIBLE
+                                Return possible variant values (those found in
+                                any installed package) for only the variants set
+                                in the image.
+
+                        VARIANT_INSTALLED
+                                Return only the variants listed in installed
+                                packages.
+
+                        VARIANT_INSTALLED_POSSIBLE
+                                Return possible variant values (those found in
+                                any installed package) for only the variants
+                                listed in installed packages.
+
+                'patterns' is an optional list of variant wildcard strings to
+                filter results by."""
+
+                variants = self._img.cfg.variants
+                if variant_list != self.VARIANT_INSTALLED and \
+                    variant_list != self.VARIANT_INSTALLED_POSSIBLE:
+                        # Include all variants set in image.
+                        vimg = set(variants.keys())
+                else:
+                        # Don't include any set only in image.
+                        vimg = set()
+
+                # Get all variants found in packages and determine state.
+                vpkg = {}
+                excludes = self._img.list_excludes()
+                vposs = collections.defaultdict(set)
+                if variant_list != self.VARIANT_IMAGE:
+                        # Only incur the overhead of reading through all
+                        # installed packages if not just listing variants set in
+                        # image or listing possible values for them.
+                        for f in self._img.gen_installed_pkgs():
+                                # The manifest must be loaded without
+                                # pre-applying excludes so that gen_variants()
+                                # can choose how to filter the actions.
+                                mfst = self._img.get_manifest(f,
+                                    ignore_excludes=True)
+                                for variant, vals in mfst.gen_variants(
+                                    excludes=excludes):
+                                        # Unlike facets, Variants class doesn't
+                                        # handle implicitly set values.
+                                        if variant[:14] == "variant.debug.":
+                                                # Debug variants are implicitly
+                                                # false and are not required
+                                                # to be set explicitly in the
+                                                # image.
+                                                vpkg[variant] = variants.get(
+                                                    variant, "false")
+                                        elif variant not in vimg:
+                                                # Although rare, packages with
+                                                # unknown variants (those not
+                                                # set in the image) can be
+                                                # installed as long as content
+                                                # does not conflict.  For those
+                                                # variants, return None.
+                                                vpkg[variant] = \
+                                                    variants.get(variant)
+
+                                        if (variant_list == \
+                                            self.VARIANT_ALL_POSSIBLE or
+                                            variant_list == \
+                                                self.VARIANT_IMAGE_POSSIBLE or
+                                            variant_list == \
+                                                self.VARIANT_INSTALLED_POSSIBLE):
+                                                # Build possible list of variant
+                                                # values.
+                                                vposs[variant].update(set(vals))
+
+                # Generate the results.
+                for name in misc.yield_matching("variant.",
+                    sorted(vimg | set(vpkg.keys())), patterns):
+                        try:
+                                yield (name, vpkg[name], sorted(vposs[name]))
+                        except KeyError:
+                                yield (name, variants[name],
+                                    sorted(vposs[name]))
+
         def freeze_pkgs(self, fmri_strings, dry_run=False, comment=None,
             unfreeze=False):
                 """Freeze/Unfreeze one or more packages."""
@@ -3915,7 +4093,7 @@
                                         pub = name = version = None
 
                                 links = hardlinks = files = dirs = \
-                                    size = licenses = cat_info = \
+                                    csize = size = licenses = cat_info = \
                                     description = None
 
                                 if PackageInfo.CATEGORIES in info_needed:
@@ -3966,7 +4144,7 @@
                                                     mfst, alt_pub=alt_pub)
 
                                         if PackageInfo.SIZE in info_needed:
-                                                size = mfst.get_size(
+                                                size, csize = mfst.get_size(
                                                     excludes=excludes)
 
                                         if act_opts & info_needed:
@@ -3987,7 +4165,7 @@
                                                             mfst.gen_key_attribute_value_by_type(
                                                             "dir", excludes))
                                 elif PackageInfo.SIZE in info_needed:
-                                        size = 0
+                                        size = csize = 0
 
                                 # Trim response set.
                                 if PackageInfo.STATE in info_needed:
@@ -4016,7 +4194,7 @@
                                     states=states, publisher=pub, version=release,
                                     build_release=build_release, branch=branch,
                                     packaging_date=packaging_date, size=size,
-                                    pfmri=pfmri, licenses=licenses,
+                                    csize=csize, pfmri=pfmri, licenses=licenses,
                                     links=links, hardlinks=hardlinks, files=files,
                                     dirs=dirs, dependencies=dependencies,
                                     description=description, attrs=attrs))
--- a/src/modules/client/imageplan.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/client/imageplan.py	Wed Jun 19 15:54:08 2013 -0700
@@ -32,7 +32,6 @@
 import mmap
 import operator
 import os
-import simplejson as json
 import stat
 import sys
 import tempfile
@@ -45,7 +44,6 @@
 import pkg.actions
 import pkg.actions.driver as driver
 import pkg.catalog
-import pkg.client.actuator as actuator
 import pkg.client.api_errors as api_errors
 import pkg.client.indexer as indexer
 import pkg.client.pkg_solver as pkg_solver
@@ -234,7 +232,8 @@
                 return self.pd._cbytes_avail
 
         def __vector_2_fmri_changes(self, installed_dict, vector,
-            li_pkg_updates=True, new_variants=None, new_facets=None):
+            li_pkg_updates=True, new_variants=None, new_facets=None,
+            fmri_changes=None):
                 """Given an installed set of packages, and a proposed vector
                 of package changes determine what, if any, changes should be
                 made to the image.  This takes into account different
@@ -242,37 +241,26 @@
                 where the only packages being updated are linked image
                 constraints, etc."""
 
-                cat = self.image.get_catalog(self.image.IMG_CATALOG_KNOWN)
-
                 fmri_updates = []
+                if fmri_changes is not None:
+                        affected = [f[0] for f in fmri_changes]
+                else:
+                        affected = None
+
                 for a, b in ImagePlan.__dicts2fmrichanges(installed_dict,
                     ImagePlan.__fmris2dict(vector)):
                         if a != b:
                                 fmri_updates.append((a, b))
                                 continue
-                        if new_facets is not None or new_variants:
-                                #
-                                # In the case of a facet change we reinstall
-                                # packages since any action in a package could
-                                # have a facet attached to it.
-                                #
-                                # In the case of variants packages should
-                                # declare what variants they contain.  Hence,
-                                # theoretically, we should be able to reduce
-                                # the number of package reinstalls by removing
-                                # re-installs of packages that don't declare
-                                # variants.  But unfortunately we've never
-                                # enforced this requirement that packages with
-                                # action variant tags declare their variants.
-                                # So now we're stuck just re-installing every
-                                # package.  sigh.
-                                #
-                                fmri_updates.append((a, b))
-                                continue
-
-                if not fmri_updates:
-                        # no planned fmri changes
-                        return []
+
+                        if (new_facets is not None or new_variants):
+                                if affected is None or a in affected:
+                                        # If affected list of packages has not
+                                        # been predetermined for package fmris
+                                        # that are unchanged, or if the fmri
+                                        # exists in the list of affected
+                                        # packages, add it to the list.
+                                        fmri_updates.append((a, a))
 
                 if fmri_updates and not li_pkg_updates:
                         # oops.  the caller requested no package updates and
@@ -288,18 +276,9 @@
 
                 self.pd._image_lm = self.image.get_last_modified(string=True)
 
-        def __plan_install_solver(self, li_pkg_updates=True, li_sync_op=False,
-            new_facets=None, new_variants=None, pkgs_inst=None,
-            reject_list=misc.EmptyI):
-                """Use the solver to determine the fmri changes needed to
-                install the specified pkgs, sync the specified image, and/or
-                change facets/variants within the current image."""
-
-                if not (new_variants or pkgs_inst or li_sync_op or
-                    new_facets is not None):
-                        # nothing to do
-                        self.pd._fmri_changes = []
-                        return
+        def __evaluate_varcets(self, new_variants, new_facets):
+                """Private helper function used to determine new facet and
+                variant state for image."""
 
                 old_facets = self.image.cfg.facets
                 if new_variants or \
@@ -319,6 +298,18 @@
                 if new_facets == old_facets:
                         new_facets = None
 
+                self.__new_excludes = self.image.list_excludes(new_variants,
+                    new_facets)
+
+                return new_variants, new_facets
+
+        def __plan_install_solver(self, li_pkg_updates=True, li_sync_op=False,
+            new_facets=None, new_variants=None, pkgs_inst=None,
+            reject_list=misc.EmptyI, fmri_changes=None):
+                """Use the solver to determine the fmri changes needed to
+                install the specified pkgs, sync the specified image, and/or
+                change facets/variants within the current image."""
+
                 # get ranking of publishers
                 pub_ranks = self.image.get_publisher_ranks()
 
@@ -341,9 +332,6 @@
                 else:
                         inst_dict = {}
 
-                self.__new_excludes = self.image.list_excludes(new_variants,
-                    new_facets)
-
                 if new_variants:
                         variants = new_variants
                 else:
@@ -374,7 +362,8 @@
                 self.pd._fmri_changes = self.__vector_2_fmri_changes(
                     installed_dict, new_vector,
                     li_pkg_updates=li_pkg_updates,
-                    new_variants=new_variants, new_facets=new_facets)
+                    new_variants=new_variants, new_facets=new_facets,
+                    fmri_changes=fmri_changes)
 
                 self.pd._solver_summary = str(solver)
                 if DebugValues["plan"]:
@@ -388,6 +377,20 @@
                 current image."""
 
                 self.__plan_op()
+
+                new_variants, new_facets = self.__evaluate_varcets(new_variants,
+                    new_facets)
+
+                if not (new_variants or pkgs_inst or li_sync_op or
+                    new_facets is not None):
+                        # nothing to do
+                        self.pd._fmri_changes = []
+                        self.pd.state = plandesc.EVALUATED_PKGS
+                        return
+
+                # If we ever actually support changing facets and variants at
+                # the same time as performing an install, the optimizations done
+                # for plan_change_varacets should be applied here (shared).
                 self.__plan_install_solver(
                     li_pkg_updates=li_pkg_updates,
                     li_sync_op=li_sync_op,
@@ -420,13 +423,144 @@
                 self.__plan_install(pkgs_inst=pkgs_inst,
                      reject_list=reject_list)
 
+        def __get_attr_fmri_changes(self, get_mattrs): 
+                # Attempt to optimize package planning by determining which
+                # packages are actually affected by changing attributes (e.g.,
+                # facets, variants).  This also provides an accurate list of
+                # affected packages as a side effect (normally, all installed
+                # packages are seen as changed).  This assumes that facets and
+                # variants are not both changing at the same time.
+                use_solver = False
+                cat = self.image.get_catalog(
+                    self.image.IMG_CATALOG_INSTALLED)
+                cat_info = frozenset([cat.DEPENDENCY])
+
+                fmri_changes = []
+                pt = self.__progtrack
+                rem_pkgs = self.image.count_installed_pkgs()
+
+                pt.plan_start(pt.PLAN_PKGPLAN, goal=rem_pkgs)
+                for f in self.image.gen_installed_pkgs():
+                        m = self.image.get_manifest(f,
+                            ignore_excludes=True)
+
+                        # Get the list of attributes involved in this operation
+                        # that the package uses and that have changed.
+                        use_solver, mattrs = get_mattrs(m, use_solver)
+                        if not mattrs:
+                                # Changed attributes unused.
+                                pt.plan_add_progress(pt.PLAN_PKGPLAN)
+                                rem_pkgs -= 1
+                                continue
+
+                        # Changed attributes are used in this package.
+                        fmri_changes.append((f, f))
+
+                        # If any dependency actions are tagged with one
+                        # of the changed attributes, assume the solver
+                        # must be used.
+                        for act in cat.get_entry_actions(f, cat_info):
+                                for attr in mattrs:
+                                        if use_solver:
+                                                break
+                                        if (act.name == "depend" and
+                                            attr in act.attrs):
+                                                use_solver = True
+                                                break
+                                if use_solver:
+                                        break
+
+                        rem_pkgs -= 1
+                        pt.plan_add_progress(pt.PLAN_PKGPLAN)
+
+                pt.plan_done(pt.PLAN_PKGPLAN)
+                pt.plan_all_done()
+
+                return use_solver, fmri_changes
+
         def plan_change_varcets(self, new_facets=None, new_variants=None,
             reject_list=misc.EmptyI):
                 """Determine the fmri changes needed to change the specified
                 facets/variants."""
 
-                self.__plan_install(new_facets=new_facets,
-                     new_variants=new_variants, reject_list=reject_list)
+                self.__plan_op()
+                new_variants, new_facets = self.__evaluate_varcets(new_variants,
+                    new_facets)
+
+                if not new_variants and new_facets is None:
+                        # nothing to do
+                        self.pd._fmri_changes = []
+                        self.pd.state = plandesc.EVALUATED_PKGS
+                        return
+
+                # By default, we assume the solver must be used.  If any of the
+                # optimizations below can be applied, they'll determine whether
+                # the solver can be used.
+                use_solver = True
+                fmri_changes = None
+
+                # The following use_solver, fmri_changes checks are only known
+                # to work correctly if only facets or only variants are
+                # changing; not both.  Changing both is not currently supported
+                # anyway so this shouldn't be a problem.
+                if new_facets is not None and not new_variants:
+                        old_facets = self.image.cfg.facets
+
+                        def get_fattrs(m, use_solver):
+                                # Get the list of facets involved in this
+                                # operation that the package uses.  To
+                                # accurately determine which packages are
+                                # actually being changed, we must compare the
+                                # old effective value for each facet that is
+                                # changing with its new effective value.
+                                return use_solver, list(
+                                    f
+                                    for f in m.gen_facets(
+                                        excludes=self.__new_excludes,
+                                        patterns=self.pd._changed_facets)
+                                    if new_facets[f] != old_facets[f]
+                                )
+
+                        use_solver, fmri_changes = \
+                            self.__get_attr_fmri_changes(get_fattrs)
+
+                if new_variants and new_facets is None:
+                        nvariants = self.pd._new_variants
+
+                        def get_vattrs(m, use_solver):
+                                # Get the list of variants involved in this
+                                # operation that the package uses.
+                                mvars = []
+                                for (variant, pvals) in m.gen_variants(
+                                    excludes=self.__new_excludes,
+                                    patterns=nvariants
+                                ):
+                                        if nvariants[variant] not in pvals:
+                                                # If the new value for the
+                                                # variant is unsupported by this
+                                                # package, then the solver
+                                                # should be triggered so the
+                                                # package can be removed.
+                                                use_solver = True
+                                        mvars.append(variant)
+                                return use_solver, mvars
+
+                        use_solver, fmri_changes = \
+                            self.__get_attr_fmri_changes(get_vattrs)
+
+                if use_solver:
+                        self.__plan_install_solver(
+                            fmri_changes=fmri_changes,
+                            new_facets=new_facets,
+                            new_variants=new_variants,
+                            reject_list=reject_list)
+                else:
+                        # If solver isn't involved, assume the list of packages
+                        # has been determined.
+                        self.pd._fmri_changes = fmri_changes and \
+                            fmri_changes or []
+
+                self.pd.state = plandesc.EVALUATED_PKGS
 
         def plan_set_mediators(self, new_mediators):
                 """Determine the changes needed to set the specified mediators.
@@ -880,7 +1014,7 @@
 
                 # disallow mount points for safety's sake.
                 if my_dev != os.stat(os.path.dirname(dir_loc)).st_dev:
-                                return [], []
+                        return [], []
 
                 # Any explicit or implicitly packaged directories are
                 # ignored; checking all directory entries is cheap.
@@ -2272,7 +2406,7 @@
                                 if str(pfmri.version) == "0,5.11" \
                                     and containing_fmri.pkg_name \
                                     not in installed_dict:
-                                                return True
+                                        return True
                                 else:
                                         pfmri.pkg_name = \
                                             containing_fmri.pkg_name
@@ -2326,7 +2460,7 @@
                         for note in self.pd.release_notes[1]:
                                 if isinstance(note, unicode):
                                         note = note.encode("utf-8")
-                                print >>tmpfile, note
+                                print >> tmpfile, note
                         tmpfile.close()
                         self.pd.release_notes_name = os.path.basename(path)
 
--- a/src/modules/client/plandesc.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/client/plandesc.py	Wed Jun 19 15:54:08 2013 -0700
@@ -481,10 +481,13 @@
                 """Returns a formatted list of strings representing the
                 variant/facet changes in this plan"""
                 vs, fs = self.varcets
-                ret = []
-                ret.extend(["variant %s: %s" % a for a in vs])
-                ret.extend(["  facet %s: %s" % a for a in fs])
-                return ret
+                return list(itertools.chain((
+                    "variant %s: %s" % (name[8:], val)
+                    for (name, val) in vs
+                ), (
+                    "  facet %s: %s" % (name[6:], val)
+                    for (name, val) in fs
+                )))
 
         def get_changes(self):
                 """A generation function that yields tuples of PackageInfo
@@ -501,10 +504,17 @@
                 and 'dest_pi' is the new version of the package it is
                 being upgraded to."""
 
-                for pp in sorted(self.pkg_plans,
-                    key=operator.attrgetter("origin_fmri", "destination_fmri")):
-                        yield (PackageInfo.build_from_fmri(pp.origin_fmri),
-                            PackageInfo.build_from_fmri(pp.destination_fmri))
+                key = operator.attrgetter("origin_fmri", "destination_fmri")
+                for pp in sorted(self.pkg_plans, key=key):
+                        sfmri = pp.origin_fmri
+                        dfmri = pp.destination_fmri
+                        if sfmri == dfmri:
+                                sinfo = dinfo = PackageInfo.build_from_fmri(
+                                    sfmri)
+                        else:
+                                sinfo = PackageInfo.build_from_fmri(sfmri)
+                                dinfo = PackageInfo.build_from_fmri(dfmri)
+                        yield (sinfo, dinfo)
 
         def get_actions(self):
                 """A generator function that yields action change descriptions
--- a/src/modules/gui/misc_non_gui.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/gui/misc_non_gui.py	Wed Jun 19 15:54:08 2013 -0700
@@ -41,7 +41,7 @@
 
 # The current version of the Client API the PM, UM and
 # WebInstall GUIs have been tested against and are known to work with.
-CLIENT_API_VERSION = 73
+CLIENT_API_VERSION = 75
 LOG_DIR = "/var/tmp"
 LOG_ERROR_EXT = "_error.log"
 LOG_INFO_EXT = "_info.log"
--- a/src/modules/gui/preferences.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/gui/preferences.py	Wed Jun 19 15:54:08 2013 -0700
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
 import g11nsvc as g11nsvc
@@ -32,7 +32,7 @@
         import pygtk
         pygtk.require("2.0")
 except ImportError:
-        sys.exit(1)        
+        sys.exit(1)
 import pkg.client.api_errors as api_errors
 import pkg.client.api as api
 import pkg.gui.misc as gui_misc
@@ -536,16 +536,15 @@
                                 break
                         facets = None
                         if manifest != None:
-                                manifest.gen_actions(())
-                                facets = manifest.facets
+                                facets = list(manifest.gen_facets())
                         if debug and facets != None:
                                 print "DEBUG facets from system/locale:", facets
 
                         facetlocales = []
                         if facets == None:
                                 return facetlocales
-                        
-                        for facet_key in facets.keys():
+
+                        for facet_key in facets:
                                 if not facet_key.startswith(LOCALE_PREFIX):
                                         continue
                                 m = re.match(LOCALE_MATCH, facet_key)
@@ -629,7 +628,7 @@
                         selected = row[enumerations.LOCALE_SELECTED]
                         locale = row[enumerations.LOCALE]
                         lang = locale.split("_")[0]
-                        
+
                         if not lang_locale_dict.has_key(lang):
                                 lang_locale_dict[lang] = {}
                         lang_locale_dict[lang][locale] = selected
@@ -766,7 +765,7 @@
 
         def __on_preferencescancel_clicked(self, widget):
                 self.w_preferencesdialog.hide()
-                
+
         def __on_preferencesclose_clicked(self, widget):
                 error_dialog_title = _("Preferences")
                 text = self.w_gsig_name_entry.get_text()
--- a/src/modules/lint/engine.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/lint/engine.py	Wed Jun 19 15:54:08 2013 -0700
@@ -40,7 +40,7 @@
 import urllib2
 
 PKG_CLIENT_NAME = "pkglint"
-CLIENT_API_VERSION = 73
+CLIENT_API_VERSION = 75
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME
 
 class LintEngineException(Exception):
--- a/src/modules/manifest.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/manifest.py	Wed Jun 19 15:54:08 2013 -0700
@@ -29,11 +29,12 @@
 import hashlib
 import os
 import tempfile
-from itertools import groupby, chain, repeat, izip
+from itertools import groupby, chain, product, repeat, izip
 from operator import itemgetter
 
 import pkg.actions as actions
 import pkg.client.api_errors as apx
+import pkg.facet as facet
 import pkg.misc as misc
 import pkg.portable as portable
 import pkg.variant as variant
@@ -123,8 +124,7 @@
                 self.fmri = pfmri
 
                 self._cache = {}
-                self._facets = None     # facets seen in package
-                self._variants = None   # variants seen in package
+                self._absent_cache = []
                 self.actions = []
                 self.actions_bytype = {}
                 self.attributes = {} # package-wide attributes
@@ -367,13 +367,267 @@
                                 )
                                 yield a
 
+        def _gen_attrs_to_str(self):
+                """Generate set action supplemental data containing all facets
+                and variants from self.actions and size information.  Each
+                returned line must be newline-terminated."""
+
+                emit_variants = "pkg.variant" not in self
+                emit_facets = "pkg.facet" not in self
+                emit_sizes = "pkg.size" not in self and "pkg.csize" not in self
+
+                if not any((emit_variants, emit_facets, emit_sizes)):
+                        # Package already has these attributes.
+                        return
+
+                # List of possible variants and possible values for them.
+                variants = defaultdict(set)
+
+                # Seed with declared set of variants as actions may be common to
+                # both and so will not be tagged with variant.
+                for name in self.attributes:
+                        if name[:8] == "variant.":
+                                variants[name] = set(self.attributes[name])
+
+                # List of possible facets and under what variant combinations
+                # they were seen.
+                facets = defaultdict(set)
+
+                # Unique (facet, value) (variant, value) combinations.
+                refs = defaultdict(lambda: defaultdict(int))
+
+                for a in self.gen_actions():
+                        name = a.name
+                        attrs = a.attrs
+                        if name == "set":
+                                if attrs["name"][:12] == "pkg.variant":
+                                        emit_variants = False
+                                elif attrs["name"][:9] == "pkg.facet":
+                                        emit_facets = False
+
+                        afacets = []
+                        avariants = []
+                        for attr, val in attrs.iteritems():
+                                if attr[:8] == "variant.":
+                                        variants[attr].add(val)
+                                        avariants.append((attr, val))
+                                elif attr[:6] == "facet.":
+                                        afacets.append((attr, val))
+
+                        for name, val in afacets:
+                                # Facet applicable to this particular variant
+                                # combination.
+                                varkey = tuple(sorted(avariants))
+                                facets[varkey].add(name)
+
+                        # This *must* be sorted to ensure reproducible set
+                        # action generation for sizes and to ensure each
+                        # combination is actually unique.
+                        varcetkeys = tuple(sorted(chain(afacets, avariants)))
+                        refs[varcetkeys]["csize"] += misc.get_pkg_otw_size(a)
+                        if name == "signature":
+                                refs[varcetkeys]["csize"] += \
+                                    a.get_action_chain_csize()
+                        refs[varcetkeys]["size"] += a.get_size()
+
+                # Prevent scope leak.
+                afacets = avariants = attrs = varcetkeys = None
+
+                if emit_variants:
+                        # Unnecessary if we can guarantee all variants will be
+                        # declared at package level.  Omit the "variant." prefix
+                        # from attribute values since that's implicit and can be
+                        # added back when the action is parsed.
+                        yield "%s\n" % AttributeAction(None, name="pkg.variant",
+                            value=sorted(v[8:] for v in variants))
+
+                # Emit a set action for every variant used with possible values
+                # if one does not already exist.
+                for name in variants:
+                        # merge_facets needs the variant values sorted and this
+                        # is desirable when generating the variant attr anyway.
+                        variants[name] = sorted(variants[name])
+                        if name not in self.attributes:
+                                yield "%s\n" % AttributeAction(None, name=name,
+                                    value=variants[name])
+
+                if emit_facets:
+                        # Get unvarianted facet set.
+                        cfacets = facets.pop((), set())
+
+                        # For each variant combination, remove unvarianted
+                        # facets since they are common to all variants.
+                        for varkey, fnames in facets.items():
+                                fnames.difference_update(cfacets)
+                                if not fnames:
+                                        # No facets unique to this combo;
+                                        # discard.
+                                        del facets[varkey]
+
+                        # If all possible variant combinations supported by the
+                        # package have at least one facet, then the intersection
+                        # of facets for all variants can be merged with the
+                        # common set.
+                        merge_facets = len(facets) > 0
+                        if merge_facets:
+                                # Determine unique set of variant combinations
+                                # seen for faceted actions.
+                                vcombos = set((
+                                    tuple(
+                                        vpair[0]
+                                        for vpair in varkey
+                                    )
+                                    for varkey in facets
+                                ))
+
+                                # For each unique variant combination, determine
+                                # if the cartesian product of all variant values
+                                # supported by the package for the combination
+                                # has been seen.  In other words, if the
+                                # combination is ((variant.arch,)) and the
+                                # package supports (i386, sparc), then both
+                                # (variant.arch, i386) and (variant.arch, sparc)
+                                # must exist.  This code assumes variant values
+                                # for each variant are already sorted.
+                                for pair in chain.from_iterable(
+                                    product(*(
+                                        tuple((name, val)
+                                            for val in variants[name])
+                                        for name in vcombo)
+                                    )
+                                    for vcombo in vcombos
+                                ):
+                                        if pair not in facets:
+                                                # If any combination the package
+                                                # supports has not been seen for
+                                                # one or more facets, then some
+                                                # facets are unique to one or
+                                                # more combinations.
+                                                merge_facets = False
+                                                break
+
+                        if merge_facets:
+                                # Merge the facets common to all variants if safe;
+                                # if we always merged them, then facets only
+                                # used by a single variant (think i386-only or
+                                # sparc-only content) would be seen unvarianted
+                                # (that's bad).
+                                vfacets = facets.values()
+                                vcfacets = vfacets[0].intersection(*vfacets[1:])
+
+                                if vcfacets:
+                                        # At least one facet is shared between
+                                        # all variant combinations; move the
+                                        # common ones to the unvarianted set.
+                                        cfacets.update(vcfacets)
+
+                                        # Remove facets common to all combos.
+                                        for varkey, fnames in facets.items():
+                                                fnames.difference_update(vcfacets)
+                                                if not fnames:
+                                                        # No facets unique to
+                                                        # this combo; discard.
+                                                        del facets[varkey]
+
+                        # Omit the "facet." prefix from attribute values since
+                        # that's implicit and can be added back when the action
+                        # is parsed.
+                        val = sorted(f[6:] for f in cfacets)
+                        if not val:
+                                # If we don't do this, action stringify will
+                                # emit this as "set name=pkg.facet" which is
+                                # then transformed to "set name=name
+                                # value=pkg.facet".  Not what we wanted, but is
+                                # expected for historical reasons.
+                                val = ""
+
+                        # Always emit an action enumerating the list of facets
+                        # common to all variants, even if there aren't any.
+                        # That way if there are also no variant-specific facets,
+                        # package operations will know that no facets are used
+                        # by the package instead of having to scan the whole
+                        # manifest.
+                        yield "%s\n" % AttributeAction(None,
+                            name="pkg.facet.common", value=val)
+
+                        # Now emit a pkg.facet action for each variant
+                        # combination containing the list of facets unique to
+                        # that combination.
+                        for varkey, fnames in facets.iteritems():
+                                # A unique key for each combination is needed,
+                                # and using a hash obfuscates that interface
+                                # while giving us a reliable way to generate
+                                # a reproducible, unique identifier.  The key
+                                # string below looks like this before hashing:
+                                #     variant.archi386variant.debug.osnetTrue...
+                                key = hashlib.sha1(
+                                    "".join("%s%s" % v for v in varkey)
+                                ).hexdigest()
+
+                                # Omit the "facet." prefix from attribute values
+                                # since that's implicit and can be added back
+                                # when the action is parsed.
+                                act = AttributeAction(None,
+                                    name="pkg.facet.%s" % key,
+                                    value=sorted(f[6:] for f in fnames))
+                                attrs = act.attrs
+                                # Tag action with variants.
+                                for v in varkey:
+                                        attrs[v[0]] = v[1]
+                                yield "%s\n" % act
+
+                # Emit pkg.[c]size attribute for [compressed] size of package
+                # for each facet/variant combination.
+                csize = 0
+                size = 0
+                for varcetkeys in refs:
+                        rcsize = refs[varcetkeys]["csize"]
+                        rsize = refs[varcetkeys]["size"]
+
+                        if not varcetkeys:
+                                # For unfaceted/unvarianted actions, keep a
+                                # running total so a single [c]size action can
+                                # be generated.
+                                csize += rcsize
+                                size += rsize
+                                continue
+
+                        if emit_sizes and (rcsize > 0 or rsize > 0):
+                                # Only emit if > 0; actions may be
+                                # faceted/variant without payload.
+
+                                # A unique key for each combination is needed,
+                                # and using a hash obfuscates that interface
+                                # while giving us a reliable way to generate
+                                # a reproducible, unique identifier.  The key
+                                # string below looks like this before hashing:
+                                #     facet.docTruevariant.archi386...
+                                key = hashlib.sha1(
+                                    "".join("%s%s" % v for v in varcetkeys)
+                                ).hexdigest()
+
+                                # The sizes are abbreviated in the name of byte
+                                # conservation.
+                                act = AttributeAction(None,
+                                    name="pkg.sizes.%s" % key,
+                                    value=["csz=%s" % rcsize, "sz=%s" % rsize])
+                                attrs = act.attrs
+                                for v in varcetkeys:
+                                        attrs[v[0]] = v[1]
+                                yield "%s\n" % act
+
+                if emit_sizes:
+                        act = AttributeAction(None, name="pkg.sizes.common",
+                            value=["csz=%s" % csize, "sz=%s" % size])
+                        yield "%s\n" % act
+
         def _actions_to_dict(self, references):
                 """create dictionary of all actions referenced explicitly or
                 implicitly from self.actions... include variants as values;
                 collapse variants where possible"""
 
                 refs = {}
-                # build a dictionary containing all directories tagged w/
+                # build a dictionary containing all actions tagged w/
                 # variants
                 for a in self.actions:
                         v, f = a.get_varcet_keys()
@@ -419,6 +673,117 @@
 
                 return list(s)
 
+        def gen_facets(self, excludes=EmptyI, patterns=EmptyI):
+                """A generator function that returns the supported facet
+                attributes (strings) for this package based on the specified (or
+                current) excludes that also match at least one of the patterns
+                provided.  Facets must be true or false so a list of possible
+                facet values is not returned."""
+
+                if self.excludes == excludes:
+                        excludes = EmptyI
+                assert excludes == EmptyI or self.excludes == EmptyI
+
+                try:
+                        facets = self["pkg.facet"]
+                except KeyError:
+                        facets = None
+
+                if facets is not None and excludes == EmptyI:
+                        # No excludes? Then use the pre-determined set of
+                        # facets.
+                        for f in misc.yield_matching("facet.", facets, patterns):
+                                yield f
+                        return
+
+                # If different excludes were specified, then look for pkg.facet
+                # actions containing the list of facets.
+                found = False
+                seen = set()
+                for a in self.gen_actions_by_type("set", excludes=excludes):
+                        if a.attrs["name"][:10] == "pkg.facet.":
+                                # Either a pkg.facet.common action or a
+                                # pkg.facet.X variant-specific action.
+                                found = True
+                                val = a.attrlist("value")
+                                if len(val) == 1 and val[0] == "":
+                                        # No facets.
+                                        continue
+
+                                for f in misc.yield_matching("facet.", (
+                                    "facet.%s" % n
+                                    for n in val
+                                ), patterns):
+                                        if f in seen:
+                                                # Prevent duplicates; it's
+                                                # possible a given facet may be
+                                                # valid for more than one unique
+                                                # variant combination that's
+                                                # allowed by current excludes.
+                                                continue
+
+                                        seen.add(f)
+                                        yield f
+
+                if not found:
+                        # Fallback to sifting actions to yield possible.
+                        facets = self._get_varcets(excludes=excludes)[1]
+                        for f in misc.yield_matching("facet.", facets, patterns):
+                                yield f
+
+        def gen_variants(self, excludes=EmptyI, patterns=EmptyI):
+                """A generator function that yields a list of tuples of the form
+                (variant, [values]).  Where 'variant' is the variant attribute
+                name (e.g. 'variant.arch') and '[values]' is a list of the
+                variant values supported by this package.  Variants returned are
+                those allowed by the specified (or current) excludes that also
+                match at least one of the patterns provided."""
+
+                if self.excludes == excludes:
+                        excludes = EmptyI
+                assert excludes == EmptyI or self.excludes == EmptyI
+
+                try:
+                        variants = self["pkg.variant"]
+                except KeyError:
+                        variants = None
+
+                if variants is not None and excludes == EmptyI:
+                        # No excludes? Then use the pre-determined set of
+                        # variants.
+                        for v in misc.yield_matching("variant.", variants,
+                            patterns):
+                                yield v, self.attributes.get(v, [])
+                        return
+
+                # If different excludes were specified, then look for
+                # pkg.variant action containing the list of variants.
+                found = False
+                variants = defaultdict(set)
+                for a in self.gen_actions_by_type("set", excludes=excludes):
+                        aname = a.attrs["name"]
+                        if aname == "pkg.variant":
+                                val = a.attrlist("value")
+                                if len(val) == 1 and val[0] == "":
+                                        # No variants.
+                                        return
+                                for v in val:
+                                        found = True
+                                        # Ensure variant entries exist (debug
+                                        # variants may not) via defaultdict.
+                                        variants["variant.%s" % v]
+                        elif aname[:8] == "variant.":
+                                for v in a.attrlist("value"):
+                                        found = True
+                                        variants[aname].add(v)
+
+                if not found:
+                        # Fallback to sifting actions to get possible.
+                        variants = self._get_varcets(excludes=excludes)[0]
+
+                for v in misc.yield_matching("variant.", variants, patterns):
+                        yield v, variants[v]
+
         def gen_mediators(self, excludes=EmptyI):
                 """A generator function that yields tuples of the form (mediator,
                 mediations) expressing the set of possible mediations for this
@@ -592,10 +957,9 @@
 
                 self.actions = []
                 self.actions_bytype = {}
-                self._variants = None
-                self._facets = None
                 self.attributes = {}
                 self._cache = {}
+                self._absent_cache = []
 
                 # So we could build up here the type/key_attr dictionaries like
                 # sdict and odict in difference() above, and have that be our
@@ -661,12 +1025,6 @@
                 if excludes and not action.include_this(excludes):
                         return
 
-                if self._variants:
-                        # Reset facet/variant cache if needed (if one is set,
-                        # then both are set, so only need to check for one).
-                        self._facets = None
-                        self._variants = None
-
                 self.actions.append(action)
                 try:
                         self.actions_bytype[aname].append(action)
@@ -681,14 +1039,66 @@
                 """Fill attribute array w/ set action contents."""
                 try:
                         keyvalue = action.attrs["name"]
-                        if keyvalue == "fmri":
-                                keyvalue = "pkg.fmri"
-                        if keyvalue not in self.attributes:
-                                self.attributes[keyvalue] = \
-                                    action.attrs["value"]
-                except KeyError: # ignore broken set actions
+                        if keyvalue[:10] == "pkg.sizes.":
+                                # To reduce manifest bloat, size and csize
+                                # are set on a single action so need splitting
+                                # into separate attributes.
+                                attrval = action.attrlist("value")
+                                for entry in attrval:
+                                        szname, szval = entry.split("=", 1)
+                                        if szname == "sz":
+                                                szname = "pkg.size"
+                                        elif szname == "csz":
+                                                szname = "pkg.csize"
+                                        else:
+                                                # Skip unknowns.
+                                                continue
+
+                                        self.attributes.setdefault(szname, 0)
+                                        self.attributes[szname] += int(szval)
+                                return
+                except (KeyError, TypeError, ValueError):
+                        # ignore broken set actions
                         pass
 
+                # Ensure facet and variant attributes are always lists.
+                if keyvalue[:10] == "pkg.facet.":
+                        # Possible facets list is spread over multiple actions.
+                        val = action.attrlist("value")
+                        if len(val) == 1 and val[0] == "":
+                                # No facets.
+                                val = []
+
+                        seen = self.attributes.setdefault("pkg.facet", [])
+                        for f in val:
+                                entry = "facet.%s" % f
+                                if entry not in seen:
+                                        # Prevent duplicates; it's possible a
+                                        # given facet may be valid for more than
+                                        # one unique variant combination that's
+                                        # allowed by current excludes.
+                                        seen.append(f)
+                        return
+                elif keyvalue == "pkg.variant":
+                        val = action.attrlist("value")
+                        if len(val) == 1 and val[0] == "":
+                                # No variants.
+                                val = []
+
+                        self.attributes[keyvalue] = [
+                            "variant.%s" % v
+                            for v in val
+                        ]
+                        return
+                elif keyvalue[:8] == "variant.":
+                        self.attributes[keyvalue] = action.attrlist("value")
+                        return
+
+                if keyvalue == "fmri":
+                        # Ancient manifest compatibility.
+                        keyvalue = "pkg.fmri"
+                self.attributes[keyvalue] = action.attrs["value"]
+
         @staticmethod
         def search_dict(file_path, excludes, return_line=False,
             log=None):
@@ -896,24 +1306,64 @@
                             "'false'" % ret))
 
         def get_size(self, excludes=EmptyI):
-                """Returns an integer representing the total size, in bytes, of
-                the Manifest's data payload.
+                """Returns an integer tuple of the form (size, csize), where
+                'size' represents the total uncompressed size, in bytes, of the
+                Manifest's data payload, and 'csize' represents the compressed
+                version of that.
 
-                'excludes' is a list of variants which should be allowed when
-                calculating the total.
-                """
+                'excludes' is a list of a list of variants and facets which
+                should be allowed when calculating the total."""
 
+                if self.excludes == excludes:
+                        excludes = EmptyI
+                assert excludes == EmptyI or self.excludes == EmptyI
+
+                csize = 0
                 size = 0
-                for a in self.gen_actions(excludes):
+
+                attrs = self.attributes
+                if ("pkg.size" in attrs and "pkg.csize" in attrs) and \
+                    (excludes == EmptyI or self.excludes == excludes):
+                        # If specified excludes match loaded excludes, then use
+                        # cached attributes; this is safe as manifest attributes
+                        # are reset or updated every time exclude_content,
+                        # set_content, or add_action is called.
+                        return (attrs["pkg.size"], attrs["pkg.csize"])
+
+                for a in self.gen_actions(excludes=excludes):
                         size += a.get_size()
-                return size
+                        csize += misc.get_pkg_otw_size(a)
+
+                if excludes == EmptyI:
+                        # Cache for future calls.
+                        attrs["pkg.size"] = size
+                        attrs["pkg.csize"] = csize
+
+                return (size, csize)
 
-        def __load_varcets(self):
-                """Private helper function to populate list of facets and
-                variants on-demand."""
+        def _get_varcets(self, excludes=EmptyI):
+                """Private helper function to get list of facets/variants."""
+
+                variants = defaultdict(set)
+                facets = defaultdict(set)
 
-                self._facets = {}
-                self._variants = {}
+                nexcludes = excludes
+                if nexcludes:
+                        # Facet filtering should never be applied when excluding
+                        # actions; only variant filtering.  This is ugly, but
+                        # our current variant/facet filtering system doesn't
+                        # allow you to be selective and various bits in
+                        # pkg.manifest assume you always filter on both so we
+                        # have to fake up a filter for facets.
+                        nexcludes = [
+                            x for x in excludes
+                            if x.__func__ != facet._allow_facet
+                        ]
+                        # Excludes list must always have zero or two items; so
+                        # fake second entry.
+                        nexcludes.append(lambda x: True)
+                        assert len(nexcludes) == 2
+
                 for action in self.gen_actions():
                         # append any variants and facets to manifest dict
                         attrs = action.attrs
@@ -923,29 +1373,27 @@
                                 continue
 
                         try:
-                                for v, d in chain(
-                                    izip(v_list, repeat(self._variants)),
-                                    izip(f_list, repeat(self._facets))):
-                                        try:
+                                for v, d in izip(v_list, repeat(variants)):
+                                        d[v].add(attrs[v])
+
+                                if not excludes or action.include_this(
+                                    nexcludes):
+                                        # While variants are package level (you
+                                        # can't install a package without
+                                        # setting the variant first), facets
+                                        # from the current action should only be
+                                        # included if the action is not
+                                        # excluded.
+                                        for v, d in izip(f_list, repeat(facets)):
                                                 d[v].add(attrs[v])
-                                        except KeyError:
-                                                d[v] = set([attrs[v]])
                         except TypeError:
                                 # Lists can't be set elements.
                                 raise actions.InvalidActionError(action,
                                     _("%(forv)s '%(v)s' specified multiple times") %
                                     {"forv": v.split(".", 1)[0], "v": v})
 
-        def __get_facets(self):
-                if self._facets is None:
-                        self.__load_varcets()
-                return self._facets
-
-        def __get_variants(self):
-                if self._variants is None:
-                        self.__load_varcets()
-                return self._variants
-
+                return (variants, facets)
+                
         def __getitem__(self, key):
                 """Return the value for the package attribute 'key'."""
                 return self.attributes[key]
@@ -965,9 +1413,6 @@
         def __contains__(self, key):
                 return key in self.attributes
 
-        facets = property(lambda self: self.__get_facets())
-        variants = property(lambda self: self.__get_variants())
-
 null = Manifest()
 
 class FactoredManifest(Manifest):
@@ -1056,8 +1501,6 @@
                 when downloading new manifests"""
                 self.actions = []
                 self.actions_bytype = {}
-                self._variants = None
-                self._facets = None
                 self.attributes = {}
                 self.loaded = False
 
@@ -1091,24 +1534,47 @@
                 # Ensure target cache directory and intermediates exist.
                 misc.makedirs(t_dir)
 
-                # create per-action type cache; use rename to avoid
-                # corrupt files if ^C'd in the middle
-                for n in self.actions_bytype.keys():
+                # create per-action type cache; use rename to avoid corrupt
+                # files if ^C'd in the middle.  All action types are considered
+                # so that empty cache files are created if no action of that
+                # type exists for the package (avoids full manifest loads
+                # later).
+                for n, acts in self.actions_bytype.iteritems():
                         t_prefix = "manifest.%s." % n
 
-                        fd, fn = tempfile.mkstemp(dir=t_dir, prefix=t_prefix)
-                        f = os.fdopen(fd, "wb")
+                        try:
+                                fd, fn = tempfile.mkstemp(dir=t_dir,
+                                    prefix=t_prefix)
+                        except EnvironmentError, e:
+                                raise apx._convert_error(e)
 
-                        for a in self.actions_bytype[n]:
-                                f.write("%s\n" % a)
-                        f.close()
-                        os.chmod(fn, PKG_FILE_MODE)
-                        portable.rename(fn, self.__cache_path("manifest.%s" % n))
+                        f = os.fdopen(fd, "wb")
+                        try:
+                                for a in acts:
+                                        f.write("%s\n" % a)
+                                if n == "set":
+                                        # Add supplemental action data; yes this
+                                        # does mean the cache is not the same as
+                                        # retrieved manifest, but that's ok.
+                                        # Signature verification is done using
+                                        # the raw manifest.
+                                        f.writelines(self._gen_attrs_to_str())
+                        except EnvironmentError, e:
+                                raise apx._convert_error(e)
+                        finally:
+                                f.close()
+
+                        try:
+                                os.chmod(fn, PKG_FILE_MODE)
+                                portable.rename(fn,
+                                    self.__cache_path("manifest.%s" % n))
+                        except EnvironmentError, e:
+                                raise apx._convert_error(e)
 
                 def create_cache(name, refs):
                         try:
                                 fd, fn = tempfile.mkstemp(dir=t_dir,
-                                    prefix="manifest.dircache.")
+                                    prefix=name + ".")
                                 with os.fdopen(fd, "wb") as f:
                                         f.writelines(refs())
                                 os.chmod(fn, PKG_FILE_MODE)
@@ -1215,30 +1681,66 @@
                         for a in Manifest.gen_actions_by_type(self, atype,
                             excludes):
                                 yield a
-                else:
-                        if excludes == EmptyI:
-                                excludes = self.excludes
-                        assert excludes == self.excludes or \
-                            self.excludes == EmptyI
-                        # we have a cached copy - use it
-                        mpath = self.__cache_path("manifest.%s" % atype)
+                        return
+
+                if excludes == EmptyI:
+                        excludes = self.excludes
+                assert excludes == self.excludes or self.excludes == EmptyI
 
-                        if not os.path.exists(mpath):
-                                return # no such action in this manifest
+                if atype in self._absent_cache:
+                        # No such action in the manifest; must be done *after*
+                        # asserting excludes are correct to avoid hiding
+                        # failures.
+                        return
 
+                # Assume a cached copy exists; if not, tag the action type to
+                # avoid pointless I/O later.
+                mpath = self.__cache_path("manifest.%s" % atype)
+
+                try:
                         with open(mpath, "rb") as f:
                                 for l in f:
                                         a = actions.fromstr(l.rstrip())
                                         if not excludes or \
                                             a.include_this(excludes):
                                                 yield a
+                except EnvironmentError, e:
+                        if e.errno == errno.ENOENT:
+                                self._absent_cache.append(atype)
+                                return # no such action in this manifest
+                        raise apx._convert_error(e)
 
-        def gen_mediators(self, excludes):
+        def gen_facets(self, excludes=EmptyI, patterns=EmptyI):
+                """A generator function that returns the supported facet
+                attributes (strings) for this package based on the specified (or
+                current) excludes that also match at least one of the patterns
+                provided.  Facets must be true or false so a list of possible
+                facet values is not returned."""
+
+                if not self.loaded and not self.__load_attributes():
+                        self.__load()
+                return Manifest.gen_facets(self, excludes=excludes,
+                    patterns=patterns)
+
+        def gen_variants(self, excludes=EmptyI, patterns=EmptyI):
+                """A generator function that yields a list of tuples of the form
+                (variant, [values]).  Where 'variant' is the variant attribute
+                name (e.g. 'variant.arch') and '[values]' is a list of the
+                variant values supported by this package.  Variants returned are
+                those allowed by the specified (or current) excludes that also
+                match at least one of the patterns provided."""
+
+                if not self.loaded and not self.__load_attributes():
+                        self.__load()
+                return Manifest.gen_variants(self, excludes=excludes,
+                    patterns=patterns)
+
+        def gen_mediators(self, excludes=EmptyI):
                 """A generator function that yields set actions expressing the
                 set of possible mediations for this package.
                 """
                 self.__load_cached_data("manifest.mediatorcache")
-                return Manifest.gen_mediators(self, excludes)
+                return Manifest.gen_mediators(self, excludes=excludes)
 
         def __load_attributes(self):
                 """Load attributes dictionary from cached set actions;
@@ -1253,8 +1755,21 @@
                                 if not self.excludes or \
                                     a.include_this(self.excludes):
                                         self.fill_attributes(a)
+
                 return True
 
+        def get_size(self, excludes=EmptyI):
+                """Returns an integer tuple of the form (size, csize), where
+                'size' represents the total uncompressed size, in bytes, of the
+                Manifest's data payload, and 'csize' represents the compressed
+                version of that.
+
+                'excludes' is a list of a list of variants and facets which
+                should be allowed when calculating the total."""
+                if not self.loaded and not self.__load_attributes():
+                        self.__load()
+                return Manifest.get_size(self, excludes=excludes)
+
         def __getitem__(self, key):
                 if not self.loaded and not self.__load_attributes():
                         self.__load()
--- a/src/modules/misc.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/misc.py	Wed Jun 19 15:54:08 2013 -0700
@@ -32,6 +32,7 @@
 import collections
 import datetime
 import errno
+import fnmatch
 import getopt
 import hashlib
 import itertools
@@ -497,8 +498,8 @@
         pkg.csize.  If that value isn't available, it returns pkg.size.
         If pkg.size isn't available, return zero."""
 
-        size = action.attrs.get("pkg.csize", 0)
-        if size == 0:
+        size = action.attrs.get("pkg.csize")
+        if size is None:
                 size = action.attrs.get("pkg.size", 0)
 
         return int(size)
@@ -2428,6 +2429,42 @@
                 s = s.decode("utf-8", "replace")
         return s
 
+def yield_matching(pat_prefix, items, patterns):
+        """Helper function for yielding items that match one of the provided
+        patterns."""
+
+        if patterns:
+                # Normalize patterns and determine whether to glob.
+                npatterns = []
+                for p in patterns:
+                        if pat_prefix:
+                                pat = p.startswith(pat_prefix) and \
+                                    p or (pat_prefix + p)
+                        else:
+                                pat = p
+                        if "*" in p or "?" in p:
+                                pat = re.compile(fnmatch.translate(pat)).match
+                                glob_match = True
+                        else:
+                                glob_match = False
+
+                        npatterns.append((pat, glob_match))
+                patterns = npatterns
+                npatterns = None
+
+        for item in items:
+                for (pat, glob_match) in patterns:
+                        if glob_match:
+                                if pat(item):
+                                        break
+                        elif item == pat:
+                                break
+                else:
+                        if patterns:
+                                continue
+                # No patterns or matched at least one.
+                yield item
+
 
 sigdict = defaultdict(list)
 
--- a/src/modules/server/api.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/server/api.py	Wed Jun 19 15:54:08 2013 -0700
@@ -327,8 +327,8 @@
                         states = None
 
                         links = hardlinks = files = dirs = dependencies = None
-                        summary = size = licenses = cat_info = description = \
-                            None
+                        summary = csize = size = licenses = cat_info = \
+                            description = None
 
                         if cat_opts & info_needed:
                                 summary, description, cat_info, dependencies = \
@@ -359,7 +359,8 @@
                                         licenses = self.__licenses(mfst)
 
                                 if PackageInfo.SIZE in info_needed:
-                                        size = mfst.get_size(excludes=excludes)
+                                        size, csize = mfst.get_size(
+                                            excludes=excludes)
 
                                 if act_opts & info_needed:
                                         if PackageInfo.LINKS in info_needed:
@@ -384,9 +385,10 @@
                             publisher=pub, version=release,
                             build_release=build_release, branch=branch,
                             packaging_date=packaging_date, size=size,
-                            pfmri=f, licenses=licenses, links=links,
-                            hardlinks=hardlinks, files=files, dirs=dirs,
-                            dependencies=dependencies, description=description))
+                            csize=csize, pfmri=f, licenses=licenses,
+                            links=links, hardlinks=hardlinks, files=files,
+                            dirs=dirs, dependencies=dependencies,
+                            description=description))
                 return {
                     self.INFO_FOUND: pis,
                     self.INFO_MISSING: notfound,
--- a/src/modules/server/depot.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/modules/server/depot.py	Wed Jun 19 15:54:08 2013 -0700
@@ -70,7 +70,6 @@
 
 import pkg
 import pkg.actions as actions
-import pkg.catalog as catalog
 import pkg.config as cfg
 import pkg.fmri as fmri
 import pkg.indexer as indexer
@@ -1325,22 +1324,24 @@
                 lsummary.seek(0)
 
                 self.__set_response_expires("info", 86400*365, 86400*365)
+                size, csize = m.get_size()
                 return """\
-          Name: %s
-       Summary: %s
-     Publisher: %s
-       Version: %s
- Build Release: %s
-        Branch: %s
-Packaging Date: %s
-          Size: %s
-          FMRI: %s
+           Name: %s
+        Summary: %s
+      Publisher: %s
+        Version: %s
+  Build Release: %s
+         Branch: %s
+ Packaging Date: %s
+           Size: %s
+Compressed Size: %s
+           FMRI: %s
 
 License:
 %s
 """ % (name, summary, pub, ver.release, ver.build_release,
-    ver.branch, ver.get_timestamp().strftime("%c"),
-    misc.bytes_to_str(m.get_size()), pfmri, lsummary.read())
+    ver.branch, ver.get_timestamp().strftime("%c"), misc.bytes_to_str(size),
+    misc.bytes_to_str(csize), pfmri, lsummary.read())
 
         @cherrypy.tools.response_headers(headers=[(
             "Content-Type", p5i.MIME_TYPE)])
--- a/src/pkgdep.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/pkgdep.py	Wed Jun 19 15:54:08 2013 -0700
@@ -43,7 +43,7 @@
 import pkg.publish.dependencies as dependencies
 from pkg.misc import msg, emsg, PipeError
 
-CLIENT_API_VERSION = 73
+CLIENT_API_VERSION = 75
 PKG_CLIENT_NAME = "pkgdepend"
 
 DEFAULT_SUFFIX = ".res"
--- a/src/sysrepo.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/sysrepo.py	Wed Jun 19 15:54:08 2013 -0700
@@ -59,7 +59,7 @@
 orig_cwd = None
 
 PKG_CLIENT_NAME = "pkg.sysrepo"
-CLIENT_API_VERSION = 73
+CLIENT_API_VERSION = 75
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME
 
 # exit codes
--- a/src/tests/cli/t_change_facet.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/tests/cli/t_change_facet.py	Wed Jun 19 15:54:08 2013 -0700
@@ -20,7 +20,7 @@
 # CDDL HEADER END
 #
 
-# Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
 
 import testutils
 if __name__ == "__main__":
@@ -122,7 +122,7 @@
 
                 self.pkg_image_create(self.rurl, additional_args=ic_args)
                 self.pkg("facet")
-                self.pkg("facet -H 'facet.locale*' | egrep False")
+                self.pkg("facet -H -F tsv 'facet.locale*' | egrep False")
 
                 # install a package and verify
                 alist = [self.plist[0]]
@@ -145,7 +145,7 @@
                 # are in effect
                 self.pkg("change-facet -n --parsable=0 wombat=false")
                 self.assertEqualParsable(self.output,
-                    affect_packages=alist,
+                    affect_packages=[],
                     change_facets=[["facet.wombat", False]])
 
                 # Again, but this time after removing the publisher cache data
@@ -157,7 +157,7 @@
                 self.pkg("change-facet --no-refresh -n --parsable=0 "
                     "wombat=false", su_wrap=True)
                 self.assertEqualParsable(self.output,
-                    affect_packages=alist,
+                    affect_packages=[],
                     change_facets=[["facet.wombat", False]])
 
                 # Again, but this time after removing the cache directory
@@ -168,7 +168,7 @@
                 self.pkg("change-facet --no-refresh -n --parsable=0 "
                     "wombat=false", su_wrap=True)
                 self.assertEqualParsable(self.output,
-                    affect_packages=alist,
+                    affect_packages=[],
                     change_facets=[["facet.wombat", False]])
 
                 # change to pick up another file w/ two tags and test the
@@ -252,8 +252,8 @@
                 # Test that setting a non-existent facet to True then removing
                 # it works.
                 self.pkg("change-facet -v foo=True")
-                self.pkg("facet -H")
-                self.assertEqual("facet.foo True\n", self.output)
+                self.pkg("facet -H -F tsv")
+                self.assertEqual("facet.foo\tTrue\n", self.output)
                 self.pkg("change-facet --parsable=0 foo=None")
                 self.assertEqualParsable(self.output, change_facets=[
                     ["facet.foo", None]])
@@ -291,12 +291,11 @@
                 # install a random package and make sure we don't accidentally
                 # change facets.
                 self.pkg("install [email protected]")
-                self.pkg("facet -H")
-                output = self.reduceSpaces(self.output)
+                self.pkg("facet -H -F tsv")
                 expected = (
-                    "facet.locale.fr_FR False\n"
-                    "facet.locale.fr False\n")
-                self.assertEqualDiff(expected, output)
+                    "facet.locale.fr\tFalse\n"
+                    "facet.locale.fr_FR\tFalse\n")
+                self.assertEqualDiff(expected, self.output)
                 for i in [ 0, 3, 4, 5, 6, 7 ]:
                         self.assert_file_is_there(str(i))
                 for i in [ 1, 2 ]:
@@ -306,12 +305,11 @@
                 # update an image and make sure we don't accidentally change
                 # facets.
                 self.pkg("update")
-                self.pkg("facet -H")
-                output = self.reduceSpaces(self.output)
+                self.pkg("facet -H -F tsv")
                 expected = (
-                    "facet.locale.fr_FR False\n"
-                    "facet.locale.fr False\n")
-                self.assertEqualDiff(expected, output)
+                    "facet.locale.fr\tFalse\n"
+                    "facet.locale.fr_FR\tFalse\n")
+                self.assertEqualDiff(expected, self.output)
                 for i in [ 0, 3, 4, 5, 6, 7 ]:
                         self.assert_file_is_there(str(i))
                 for i in [ 1, 2 ]:
@@ -333,10 +331,10 @@
 
                 # set a facet on an image with no facets
                 self.pkg("change-facet -v locale.fr=False")
-                self.pkg("facet -H")
+                self.pkg("facet -H -F tsv")
                 output = self.reduceSpaces(self.output)
                 expected = (
-                    "facet.locale.fr False\n")
+                    "facet.locale.fr\tFalse\n")
                 self.assertEqualDiff(expected, output)
                 for i in [ 0, 2, 3, 4, 5, 6, 7 ]:
                         self.assert_file_is_there(str(i))
@@ -346,12 +344,11 @@
 
                 # set a facet on an image with existing facets
                 self.pkg("change-facet -v locale.fr_FR=False")
-                self.pkg("facet -H")
-                output = self.reduceSpaces(self.output)
+                self.pkg("facet -H -F tsv")
                 expected = (
-                    "facet.locale.fr_FR False\n"
-                    "facet.locale.fr False\n")
-                self.assertEqualDiff(expected, output)
+                    "facet.locale.fr\tFalse\n"
+                    "facet.locale.fr_FR\tFalse\n")
+                self.assertEqualDiff(expected, self.output)
                 for i in [ 0, 3, 4, 5, 6, 7 ]:
                         self.assert_file_is_there(str(i))
                 for i in [ 1, 2 ]:
@@ -361,11 +358,11 @@
                 # clear a facet while setting a facet on an image with other
                 # facets that aren't being changed
                 self.pkg("change-facet -v locale.fr=None locale.nl=False")
-                self.pkg("facet -H")
+                self.pkg("facet -H -F tsv")
                 output = self.reduceSpaces(self.output)
                 expected = (
-                    "facet.locale.fr_FR False\n"
-                    "facet.locale.nl False\n")
+                    "facet.locale.fr_FR\tFalse\n"
+                    "facet.locale.nl\tFalse\n")
                 self.assertEqualDiff(expected, output)
                 for i in [ 0, 1, 3, 4, 6, 7 ]:
                         self.assert_file_is_there(str(i))
@@ -376,10 +373,10 @@
                 # clear a facet on an image with other facets that aren't
                 # being changed
                 self.pkg("change-facet -v locale.nl=None")
-                self.pkg("facet -H")
+                self.pkg("facet -H -F tsv")
                 output = self.reduceSpaces(self.output)
                 expected = (
-                    "facet.locale.fr_FR False\n")
+                    "facet.locale.fr_FR\tFalse\n")
                 self.assertEqualDiff(expected, output)
                 for i in [ 0, 1, 3, 4, 5, 6, 7 ]:
                         self.assert_file_is_there(str(i))
@@ -389,7 +386,7 @@
 
                 # clear the only facet on an image
                 self.pkg("change-facet -v locale.fr_FR=None")
-                self.pkg("facet -H")
+                self.pkg("facet -H -F tsv")
                 self.assertEqualDiff("", self.output)
                 for i in range(8):
                         self.assert_file_is_there(str(i))
--- a/src/tests/cli/t_change_variant.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/tests/cli/t_change_variant.py	Wed Jun 19 15:54:08 2013 -0700
@@ -20,7 +20,7 @@
 # CDDL HEADER END
 #
 
-# Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
 
 import testutils
 if __name__ == "__main__":
@@ -32,6 +32,7 @@
 import re
 import unittest
 
+import pkg.misc as misc
 from pkg.client.pkgdefs import *
 
 class TestPkgChangeVariant(pkg5unittest.SingleDepotTestCase):
@@ -69,8 +70,19 @@
         add file tmp/pkg_shared/shared/nonglobal_motd mode=0555 owner=root group=bin path=shared/zone_motd variant.opensolaris.zone=nonglobal
         add file tmp/pkg_shared/unique/global mode=0555 owner=root group=bin path=unique/global variant.opensolaris.zone=global
         add file tmp/pkg_shared/unique/nonglobal mode=0555 owner=root group=bin path=unique/nonglobal variant.opensolaris.zone=nonglobal
+        close"""
 
-        close"""
+        pkg_unknown = """
+        open [email protected]
+        add set name=variant.unknown value=bar value=foo
+        add file tmp/bar path=usr/bin/bar mode=0755 owner=root group=root variant.unknown=bar
+        add file tmp/foo path=usr/bin/foo mode=0755 owner=root group=root variant.unknown=foo
+        close
+        open [email protected]
+        add set name=variant.unknown value=bar value=foo
+        add file tmp/bar path=usr/bin/foobar mode=0755 owner=root group=root variant.unknown=bar
+        add file tmp/foo path=usr/bin/foobar mode=0755 owner=root group=root variant.unknown=foo
+        close """
 
         # this package intentionally has no variant.arch specification.
         pkg_inc = """
@@ -109,7 +121,10 @@
             "tmp/pkg_shared/shared/global_motd",
             "tmp/pkg_shared/shared/nonglobal_motd",
             "tmp/pkg_shared/unique/global",
-            "tmp/pkg_shared/unique/nonglobal"
+            "tmp/pkg_shared/unique/nonglobal",
+
+            "tmp/bar",
+            "tmp/foo"
         ]
 
         def setUp(self):
@@ -117,7 +132,8 @@
 
                 self.make_misc_files(self.misc_files)
                 self.pkgsend_bulk(self.rurl, (self.pkg_i386, self.pkg_sparc,
-                    self.pkg_shared, self.pkg_inc, self.pkg_cluster))
+                    self.pkg_shared, self.pkg_inc, self.pkg_cluster,
+                    self.pkg_unknown))
 
                 # verify pkg search indexes
                 self.verify_search = True
@@ -125,6 +141,16 @@
                 # verify installed images before changing variants
                 self.verify_install = False
 
+        def __assert_variant_matches_tsv(self, expected, errout=None,
+            exit=0, opts=misc.EmptyI, names=misc.EmptyI, su_wrap=False):
+                self.pkg("variant %s -H -F tsv %s" % (" ".join(opts),
+                    " ".join(names)), exit=exit, su_wrap=su_wrap)
+                self.assertEqualDiff(expected, self.output)
+                if errout:
+                        self.assert_(self.errout != "")
+                else:
+                        self.assertEqualDiff("", self.errout)
+
         def f_verify(self, path, token=None, negate=False):
                 """Verify that the specified path exists and contains
                 the specified token.  If negate is true, then make sure
@@ -303,8 +329,12 @@
                     "variant.opensolaris.zone": v_zone
                 }
                 self.image_create(self.rurl, variants=variants)
-                self.pkg("variant -H| egrep %s" % ("'variant.arch[ ]*%s'" % v_arch))
-                self.pkg("variant -H| egrep %s" % ("'variant.opensolaris.zone[ ]*%s'" % v_zone))
+
+                exp_tsv = """\
+variant.arch\t%s
+variant.opensolaris.zone\t%s
+""" % (v_arch, v_zone)
+                self.__assert_variant_matches_tsv(exp_tsv)
 
                 # install the specified packages into the image
                 ii_args = ""
@@ -325,8 +355,11 @@
                 # verify the updated image
                 self.i_verify(v_arch2, v_zone2, pl2)
 
-                self.pkg("variant -H| egrep %s" % ("'variant.arch[ ]*%s'" % v_arch2))
-                self.pkg("variant -H| egrep %s" % ("'variant.opensolaris.zone[ ]*%s'" % v_zone2))
+                exp_tsv = """\
+variant.arch\t%s
+variant.opensolaris.zone\t%s
+""" % (v_arch2, v_zone2)
+                self.__assert_variant_matches_tsv(exp_tsv)
 
                 self.image_destroy()
 
@@ -428,6 +461,44 @@
                 self.cv_test("sparc", "global", ["pkg_cluster"],
                     "i386", "nonglobal", ["pkg_cluster"])
 
+        def test_cv_12_unknown(self):
+                """Ensure that packages with an unknown variant and
+                non-conflicting content can be installed and subsequently
+                altered using change-variant."""
+
+                self.image_create(self.rurl)
+
+                # Install package with unknown variant and verify both files are
+                # present.
+                self.pkg("install -v [email protected]")
+                for fname in ("bar", "foo"):
+                        self.f_verify("usr/bin/%s" % fname, fname)
+
+                # Next, verify upgrade to version of package with unknown
+                # variant fails if new version delivers conflicting content and
+                # variant has not been set.
+                self.pkg("update -vvv [email protected]", exit=1)
+
+                # Next, set unknown variant explicitly and verify content
+                # changes as expected.
+                self.pkg("change-variant unknown=foo")
+
+                # Verify bar no longer exists...
+                self.f_verify("usr/bin/bar", "bar", negate=True)
+                # ...and foo still does.
+                self.f_verify("usr/bin/foo", "foo")
+
+                # Next, upgrade to version of package with conflicting content
+                # and verify content changes as expected.
+                self.pkg("update -vvv [email protected]")
+
+                # Verify bar and foo no longer exist...
+                for fname in ("bar", "foo"):
+                        self.f_verify("usr/bin/%s" % fname, fname, negate=True)
+
+                # ...and that foo variant of foobar is now installed.
+                self.f_verify("usr/bin/foobar", "foo")
+
         def test_cv_parsable(self):
                 """Test the parsable output of change-variant."""
 
--- a/src/tests/cli/t_pkg_install.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/tests/cli/t_pkg_install.py	Wed Jun 19 15:54:08 2013 -0700
@@ -8472,8 +8472,8 @@
                     "tripledupfilec")
                 self.pkg("-D broken-conflicting-action-handling=1 install "
                     "tripledupfilea")
-                self.pkg("change-variant variant.foo=two")
-                self.pkg("change-variant variant.foo=one", exit=1)
+                self.pkg("change-variant -vvv variant.foo=two")
+                self.pkg("change-variant -vvv variant.foo=one", exit=1)
 
         def dir_exists(self, path, mode=None, owner=None, group=None):
                 dir_path = os.path.join(self.get_img_path(), path)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/cli/t_pkg_varcet.py	Wed Jun 19 15:54:08 2013 -0700
@@ -0,0 +1,591 @@
+#!/usr/bin/python
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or http://www.opensolaris.org/os/licensing.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+
+# Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+
+import testutils
+if __name__ == "__main__":
+        testutils.setup_environment("../../../proto")
+import pkg5unittest
+
+import os
+import pkg.fmri as fmri
+import pkg.portable as portable
+import pkg.misc as misc
+import pkg.p5p
+import shutil
+import stat
+import tempfile
+import unittest
+
+
+class TestPkgVarcet(pkg5unittest.SingleDepotTestCase):
+
+        # Don't discard repository or setUp() every test.
+        persistent_setup = True
+
+        pkg_foo = """
+            open [email protected]
+            add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root
+            add file tmp/man path=usr/man/man1/foo.1 mode=0444 owner=root group=root
+            close
+            open [email protected]
+            add set name=variant.icecream value=neapolitan value=strawberry
+            add file tmp/debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=true
+            add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=false
+            add file tmp/neapolitan path=etc/icecream mode=0644 owner=root group=root variant.icecream=neapolitan
+            add file tmp/strawberry path=etc/icecream mode=0644 owner=root group=root variant.icecream=strawberry
+            add file tmp/man path=usr/man/man1/foo.1 mode=0444 owner=root group=root facet.doc.man=true
+            add file tmp/doc path=usr/share/foo/README mode=0444 owner=root group=root facet.doc.txt=true
+            add file tmp/pdf path=usr/share/foo/README.pdf mode=0444 owner=root group=root facet.doc.pdf=true
+            open [email protected]
+            add set name=pkg.facet value=doc.man value=doc.txt value=doc.pdf
+            add set name=pkg.variant value=icecream value=debug.foo
+            add set name=variant.icecream value=neapolitan value=strawberry
+            add file tmp/debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=true
+            add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root variant.debug.foo=false
+            add file tmp/neapolitan path=etc/icecream mode=0644 owner=root group=root variant.icecream=neapolitan
+            add file tmp/strawberry path=etc/icecream mode=0644 owner=root group=root variant.icecream=strawberry
+            add file tmp/man path=usr/man/man1/foo.1 mode=0444 owner=root group=root facet.doc.man=true
+            add file tmp/doc path=usr/share/foo/README mode=0444 owner=root group=root facet.doc.txt=true
+            add file tmp/pdf path=usr/share/foo/README.pdf mode=0444 owner=root group=root facet.doc.pdf=true
+            close """
+
+        pkg_unknown = """
+            open [email protected]
+            add set name=variant.unknown value=bar value=foo
+            add file tmp/non-debug path=usr/bin/bar mode=0755 owner=root group=root variant.unknown=bar
+            add file tmp/non-debug path=usr/bin/foo mode=0755 owner=root group=root variant.unknown=foo
+            close """
+
+        misc_files = ["tmp/debug", "tmp/non-debug", "tmp/neapolitan",
+            "tmp/strawberry", "tmp/doc", "tmp/man", "tmp/pdf"]
+
+        def setUp(self):
+                pkg5unittest.SingleDepotTestCase.setUp(self)
+                self.make_misc_files(self.misc_files)
+                self.pkgsend_bulk(self.rurl, [
+                    getattr(self, p)
+                    for p in dir(self)
+                    if p.startswith("pkg_") and isinstance(getattr(self, p),
+                        basestring)
+                ])
+
+        def __assert_varcet_matches_default(self, cmd, expected, errout=None,
+            exit=0, opts=misc.EmptyI, names=misc.EmptyI, su_wrap=False):
+                if errout is None and exit != 0:
+                        # Assume there should be error output for non-zero exit
+                        # if not explicitly indicated.
+                        errout = True
+
+                self.pkg("%s %s -H %s" % (cmd, " ".join(opts), " ".join(names)),
+                    exit=exit, su_wrap=su_wrap)
+                self.assertEqualDiff(expected, self.reduceSpaces(self.output))
+                if errout:
+                        self.assert_(self.errout != "")
+                else:
+                        self.assertEqualDiff("", self.errout)
+
+        def __assert_varcet_matches_tsv(self, cmd, expected, errout=None,
+            exit=0, opts=misc.EmptyI, names=misc.EmptyI, su_wrap=False):
+                self.pkg("%s %s -H -F tsv %s" % (cmd, " ".join(opts),
+                    " ".join(names)), exit=exit, su_wrap=su_wrap)
+                self.assertEqualDiff(expected, self.output)
+                if errout:
+                        self.assert_(self.errout != "")
+                else:
+                        self.assertEqualDiff("", self.errout)
+
+        def __assert_varcet_fails(self, cmd, operands, errout=True, exit=1,
+            su_wrap=False):
+                self.pkg("%s %s" % (cmd, operands), exit=exit, su_wrap=su_wrap) 
+                if errout:
+                        self.assert_(self.errout != "")
+                else:
+                        self.assertEqualDiff("", self.errout)
+
+        def __assert_facet_matches_default(self, *args, **kwargs):
+                self.__assert_varcet_matches_default("facet", *args,
+                    **kwargs)
+
+        def __assert_facet_matches_tsv(self, *args, **kwargs):
+                self.__assert_varcet_matches_tsv("facet", *args,
+                    **kwargs)
+
+        def __assert_facet_matches(self, exp_def, **kwargs):
+                exp_tsv = exp_def.replace(" ", "\t")
+                self.__assert_varcet_matches_default("facet", exp_def,
+                    **kwargs)
+                self.__assert_varcet_matches_tsv("facet", exp_tsv,
+                    **kwargs)
+
+        def __assert_facet_fails(self, *args, **kwargs):
+                self.__assert_varcet_fails("facet", *args, **kwargs)
+
+        def __assert_variant_matches_default(self, *args, **kwargs):
+                self.__assert_varcet_matches_default("variant", *args,
+                    **kwargs)
+
+        def __assert_variant_matches_tsv(self, *args, **kwargs):
+                self.__assert_varcet_matches_tsv("variant", *args,
+                    **kwargs)
+
+        def __assert_variant_matches(self, exp_def, **kwargs):
+                exp_tsv = exp_def.replace(" ", "\t")
+                self.__assert_varcet_matches_default("variant", exp_def,
+                    **kwargs)
+                self.__assert_varcet_matches_tsv("variant", exp_tsv,
+                    **kwargs)
+
+        def __assert_variant_fails(self, *args, **kwargs):
+                self.__assert_varcet_fails("variant", *args, **kwargs)
+
+        def __test_foo_facet_upgrade(self, pkg):
+                #
+                # Next, verify output after upgrading package to faceted
+                # version.
+                #
+                self.pkg("update %s" % pkg)
+
+                # Verify output for no options and no patterns.
+                exp_def = """\
+facet.doc.* False
+facet.doc.html False
+facet.doc.man False
+facet.doc.txt True
+"""
+                self.__assert_facet_matches(exp_def)
+
+                # Unmatched because facet is not explicitly set.
+                self.__assert_facet_fails("doc.pdf")
+                self.__assert_facet_fails("'*pdf'")
+
+                # Matched case for explicitly set.
+                exp_def = """\
+facet.doc.* False
+facet.doc.txt True
+"""
+                names = ("'facet.doc.[*]'", "doc.txt")
+                self.__assert_facet_matches(exp_def, names=names)
+
+                # Verify -a output.
+                exp_def = """\
+facet.doc.* False
+facet.doc.html False
+facet.doc.man False
+facet.doc.pdf False
+facet.doc.txt True
+"""
+                opts = ("-a",)
+                self.__assert_facet_matches(exp_def, opts=opts)
+
+                # Matched case for explicitly set and those in packages.
+                exp_def = """\
+facet.doc.* False
+facet.doc.pdf False
+facet.doc.txt True
+"""
+                names = ("'facet.doc.[*]'", "*pdf", "facet.doc.txt")
+                opts = ("-a",)
+                self.__assert_facet_matches(exp_def, opts=opts, names=names)
+
+                # Verify -i output.
+                exp_def = """\
+facet.doc.man False
+facet.doc.pdf False
+facet.doc.txt True
+"""
+                opts = ("-i",)
+                self.__assert_facet_matches(exp_def, opts=opts)
+
+                # Unmatched because facet is not used in package.
+                self.__assert_facet_fails("-i doc.html")
+                self.__assert_facet_fails("-i '*html'")
+
+                # Matched case in packages.
+                exp_def = """\
+facet.doc.man False
+facet.doc.pdf False
+"""
+                names = ("'facet.*[!t]'",)
+                opts = ("-i",)
+                self.__assert_facet_matches(exp_def, opts=opts, names=names)
+
+                exp_def = """\
+facet.doc.pdf False
+"""
+                names = ("'*pdf'",)
+                opts = ("-i",)
+                self.__assert_facet_matches(exp_def, opts=opts, names=names)
+
+                # Now uninstall package and verify output (to ensure any
+                # potentially cached information has been updated).
+                self.pkg("uninstall foo")
+
+                exp_def = """\
+facet.doc.* False
+facet.doc.html False
+facet.doc.man False
+facet.doc.txt True
+"""
+
+                # Output should be the same for both -a and default cases with
+                # no packages installed.
+                for opts in ((), ("-a",)):
+                        self.__assert_facet_matches(exp_def, opts=opts)
+
+                # No output expected for -i.
+                opts = ("-i",)
+                self.__assert_facet_matches("", opts=opts)
+
+        def test_00_facet(self):
+                """Verify facet subcommand works as expected."""
+
+                # create an image
+                variants = { "variant.icecream": "strawberry" }
+                self.image_create(self.rurl, variants=variants)
+
+                # Verify invalid options handled gracefully.
+                self.__assert_facet_fails("-z", exit=2)
+                self.__assert_facet_fails("-fi", exit=2)
+
+                #
+                # First, verify output before setting any facets or installing
+                # any packages.
+                #
+
+                # Output should be the same for all cases with no facets set and
+                # no packages installed.
+                for opts in ((), ("-i",), ("-a",)):
+                        # No operands specified case.
+                        self.__assert_facet_matches("", opts=opts)
+
+                        # Unprivileged user case.
+                        self.__assert_facet_matches("", opts=opts, su_wrap=True)
+
+                        # Unmatched case.
+                        self.__assert_facet_matches_default("", opts=opts,
+                            names=("bogus",), exit=1)
+
+                        # Unmatched case tsv; subtly different as no error
+                        # output is expected.
+                        self.__assert_facet_matches_tsv("", opts=opts,
+                            names=("bogus",), exit=1, errout=False)
+
+                #
+                # Next, verify output after setting facets.
+                #
+
+                # Set some facets.
+                self.pkg("change-facet 'doc.*=False' doc.man=False "
+                    "facet.doc.html=False facet.doc.txt=True")
+
+                exp_def = """\
+facet.doc.* False
+facet.doc.html False
+facet.doc.man False
+facet.doc.txt True
+"""
+
+                # Output should be the same for both -a and default cases with
+                # no packages installed.
+                for opts in ((), ("-a",)):
+                        self.__assert_facet_matches(exp_def, opts=opts)
+
+                #
+                # Next, verify output after installing unfaceted package.
+                #
+                self.pkg("install [email protected]")
+
+                # Verify output for no options and no patterns.
+                exp_def = """\
+facet.doc.* False
+facet.doc.html False
+facet.doc.man False
+facet.doc.txt True
+"""
+                self.__assert_facet_matches(exp_def)
+
+                # Verify -a output.
+                opts = ("-a",)
+                self.__assert_facet_matches(exp_def, opts=opts)
+
+                # Verify -i output.
+                opts = ("-i",)
+                self.__assert_facet_matches("", opts=opts)
+
+                # Test upgraded package that does not declare all
+                # facets/variants.
+                self.__test_foo_facet_upgrade("[email protected]")
+
+                # Reinstall and then retest with upgraded package that declares
+                # all facets/variants.
+                self.pkg("install [email protected]")
+                self.__test_foo_facet_upgrade("[email protected]")
+
+        def __test_foo_variant_upgrade(self, pkg, variants):
+                #
+                # Next, verify output after upgrading package to varianted
+                # version.
+                #
+                self.pkg("update %s" % pkg)
+
+                # Verify output for no options and no patterns.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.icecream strawberry
+variant.opensolaris.zone global
+""" % variants
+                self.__assert_variant_matches(exp_def)
+
+                # Unmatched because variant is not explicitly set.
+                self.__assert_variant_fails("debug.foo")
+                self.__assert_variant_fails("'*foo'")
+
+                # Matched case for explicitly set.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.opensolaris.zone global
+""" % variants
+                names = ("arch", "'variant.*zone'")
+                self.__assert_variant_matches(exp_def, names=names)
+
+                # Verify -a output.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.debug.foo false
+variant.icecream strawberry
+variant.opensolaris.zone global
+""" % variants
+                opts = ("-a",)
+                self.__assert_variant_matches(exp_def, opts=opts)
+
+                # Matched case for explicitly set and those in packages.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.debug.foo false
+variant.opensolaris.zone global
+""" % variants
+                names = ("'variant.debug.*'", "arch", "'*zone'")
+                opts = ("-a",)
+                self.__assert_variant_matches(exp_def, opts=opts, names=names)
+
+                # Verify -i output.
+                exp_def = """\
+variant.debug.foo false
+variant.icecream strawberry
+""" % variants
+                opts = ("-i",)
+                self.__assert_variant_matches(exp_def, opts=opts)
+
+                # Unmatched because variant is not used in package.
+                self.__assert_variant_fails("-i opensolaris.zone")
+                self.__assert_variant_fails("-i '*arch'")
+
+                # Verify -v and -av output.
+                exp_def = """\
+variant.debug.foo false
+variant.debug.foo true
+variant.icecream neapolitan
+variant.icecream strawberry
+"""
+                for opts in (("-v",), ("-av",)):
+                        self.__assert_variant_matches(exp_def, opts=opts)
+
+                exp_def = """\
+variant.icecream neapolitan
+variant.icecream strawberry
+""" % variants
+                names = ("'ice*'",)
+                opts = ("-av",)
+                self.__assert_variant_matches(exp_def, opts=opts, names=names)
+
+                # Matched case in packages.
+                exp_def = """\
+variant.icecream strawberry
+""" % variants
+                names = ("'variant.*[!o]'",)
+                opts = ("-i",)
+                self.__assert_variant_matches(exp_def, opts=opts, names=names)
+
+                exp_def = """\
+variant.debug.foo false
+""" % variants
+                names = ("*foo",)
+                opts = ("-i",)
+                self.__assert_variant_matches(exp_def, opts=opts, names=names)
+
+                # Now uninstall package and verify output (to ensure any
+                # potentially cached information has been updated).
+                self.pkg("uninstall foo")
+
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.icecream strawberry
+variant.opensolaris.zone global
+""" % variants
+
+                # Output should be the same for both -a and default cases with
+                # no packages installed.
+                for opts in ((), ("-a",)):
+                        self.__assert_variant_matches(exp_def, opts=opts)
+
+                # No output expected for -v, -av, -i, or -iv.
+                for opts in (("-v",), ("-av",), ("-i",), ("-iv",)):
+                        self.__assert_variant_matches("", opts=opts)
+
+        def test_01_variant(self):
+                """Verify variant subcommand works as expected."""
+
+                # create an image
+                self.image_create(self.rurl)
+
+                # Get variant data.
+                api_obj = self.get_img_api_obj()
+                variants = dict(v[:-1] for v in api_obj.gen_variants(
+                    api_obj.VARIANT_IMAGE))
+
+                # Verify invalid options handled gracefully.
+                self.__assert_variant_fails("-z", exit=2)
+                self.__assert_variant_fails("-ai", exit=2)
+                self.__assert_variant_fails("-aiv", exit=2)
+
+                #
+                # First, verify output before setting any variants or installing
+                # any packages.
+                #
+
+                # Output should be the same for -a and default cases with no
+                # variants set and no packages installed.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.opensolaris.zone global
+""" % variants
+
+                for opts in ((), ("-a",)):
+                        # No operands specified case.
+                        self.__assert_variant_matches(exp_def, opts=opts)
+
+                        # Unprivileged user case.
+                        self.__assert_variant_matches(exp_def, opts=opts,
+                            su_wrap=True)
+
+                        # Unmatched case.
+                        self.__assert_variant_matches_default("", opts=opts,
+                            names=("bogus",), exit=1)
+
+                        # Unmatched case tsv; subtly different as no error
+                        # output is expected.
+                        self.__assert_variant_matches_tsv("", opts=opts,
+                            names=("bogus",), exit=1, errout=False)
+
+                # No output expected for with no variants set and no packages
+                # installed for -v, -av, -i, and -iv.
+                for opts in (("-v",), ("-av",), ("-i",), ("-iv",)):
+                        self.__assert_variant_matches("", opts=opts)
+
+                #
+                # Next, verify output after setting variants.
+                #
+
+                # Set some variants.
+                self.pkg("change-variant variant.icecream=strawberry")
+
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.icecream strawberry
+variant.opensolaris.zone global
+""" % variants
+
+                # Output should be the same for both -a and default cases with
+                # no packages installed.
+                for opts in ((), ("-a",)):
+                        self.__assert_variant_matches(exp_def, opts=opts)
+
+                #
+                # Next, verify output after installing unvarianted package.
+                #
+                self.pkg("install [email protected]")
+
+                # Verify output for no options and no patterns.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.icecream strawberry
+variant.opensolaris.zone global
+""" % variants
+                self.__assert_variant_matches(exp_def)
+
+                # Verify -a output.
+                opts = ("-a",)
+                self.__assert_variant_matches(exp_def, opts=opts)
+
+                # Verify -v, -av, -i, and -iv output.
+                for opts in (("-v",), ("-av",), ("-i",), ("-iv",)):
+                        self.__assert_variant_matches("", opts=opts)
+
+                # Test upgraded package that does not declare all
+                # facets/variants.
+                self.__test_foo_variant_upgrade("[email protected]", variants)
+
+                # Reinstall and then retest with upgraded package that declares
+                # all facets/variants.
+                self.pkg("install [email protected]")
+                self.__test_foo_variant_upgrade("[email protected]", variants)
+
+                # Next, verify output after installing package with unknown
+                # variant.
+                self.pkg("install [email protected]")
+
+                # Verify output for no options and no patterns.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.icecream strawberry
+variant.opensolaris.zone global
+""" % variants
+                self.__assert_variant_matches(exp_def)
+
+                # Verify -a output.
+                exp_def = """\
+variant.arch %(variant.arch)s
+variant.icecream strawberry
+variant.opensolaris.zone global
+variant.unknown 
+""" % variants
+                self.__assert_variant_matches(exp_def, opts=("-a",))
+
+                # Verify -i output.
+                exp_def = """\
+variant.unknown 
+""" % variants
+                self.__assert_variant_matches(exp_def, opts=("-i",))
+
+                # Verify -v, -av, and -iv output.
+                for opts in (("-v",), ("-av",), ("-iv",)):
+                        exp_def = """\
+variant.unknown bar
+variant.unknown foo
+""" % variants
+                        self.__assert_variant_matches(exp_def, opts=opts)
+
+
+if __name__ == "__main__":
+        unittest.main()
--- a/src/tests/cli/t_pkgdep_resolve.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/tests/cli/t_pkgdep_resolve.py	Wed Jun 19 15:54:08 2013 -0700
@@ -1116,7 +1116,9 @@
                                     "\n".join(
                                         [str(d) for d in pkg_deps[col_path]]))
                         d = pkg_deps[one_dep][0]
-                        self.assertEqual(d.attrs["fmri"], exp_pkg)
+                        self.assertEqual(d.attrs["fmri"], exp_pkg,
+                            "Expected dependency %s; found %s." % (exp_pkg,
+                                d.attrs["fmri"]))
 
                 col_path = self.make_manifest(self.collision_manf)
                 col_path_num_var = self.make_manifest(
--- a/src/tests/pkg5unittest.py	Wed Jun 12 15:44:50 2013 -0700
+++ b/src/tests/pkg5unittest.py	Wed Jun 19 15:54:08 2013 -0700
@@ -132,7 +132,7 @@
 
 # Version test suite is known to work with.
 PKG_CLIENT_NAME = "pkg"
-CLIENT_API_VERSION = 73
+CLIENT_API_VERSION = 75
 
 ELIDABLE_ERRORS = [ TestSkippedException, depotcontroller.DepotStateException ]